├── .gitignore ├── LICENSE ├── README.org ├── client ├── package.json ├── rollup.config.js ├── src │ └── index.js ├── static │ └── index.html └── yarn.lock ├── color-picker.html ├── screenshots └── webkit-color-picker.gif ├── webkit-color-picker.el └── webkit-color-picker.org /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | *.elc 3 | 4 | # Packaging 5 | .cask 6 | 7 | # Backup files 8 | *~ 9 | 10 | # Undo-tree save-files 11 | *.~undo-tree 12 | 13 | /client/node_modules 14 | /client/build 15 | /client/yarn-error.log 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ozan Sener 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+AUTHOR: Ozan Sener 2 | * webkit-color-picker 3 | 4 | Small experiment with embedded a Webkit widgets in a childframe. Requires Emacs 26 compiled with [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Embedded-WebKit-Widgets.html][embedded Webkit Widget support]]. 5 | 6 | webkit-color-picker is available on [[https://melpa.org/][MELPA]]. Example configuration using [[https://github.com/jwiegley/use-package][use-package]]: 7 | 8 | #+BEGIN_SRC emacs-lisp 9 | (use-package webkit-color-picker 10 | :ensure t 11 | :bind (("C-c C-p" . webkit-color-picker-show))) 12 | #+END_SRC 13 | 14 | ** Screenshot 15 | [[file:./screenshots/webkit-color-picker.gif]] 16 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emacs-webkit-color-picker", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "browse": "browser-sync start --s --ss static --ss build --index static/index-dev.html --files static/*.html,build/*.js --no-notify", 8 | "build:js": "rollup -c", 9 | "build:html": "sed -e 's/app\\.js/app.min.js/' static/index.html | html-inline -b build -o ../color-picker.html", 10 | "build": "npm run build:js && npm run build:html", 11 | "start": "rollup -c -w" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.26.0", 15 | "babel-plugin-external-helpers": "^6.22.0", 16 | "babel-preset-env": "^1.6.1", 17 | "babel-preset-react": "^6.24.1", 18 | "babel-preset-stage-0": "^6.24.1", 19 | "browser-sync": "^2.23.6", 20 | "html-inline": "^1.2.0", 21 | "rollup": "0.52.0", 22 | "rollup-plugin-alias": "^1.4.0", 23 | "rollup-plugin-babel": "^3.0.3", 24 | "rollup-plugin-browsersync": "^0.2.6", 25 | "rollup-plugin-commonjs": "8.2.6", 26 | "rollup-plugin-node-globals": "1.1.0", 27 | "rollup-plugin-node-resolve": "3.0.0", 28 | "rollup-plugin-replace": "2.0.0", 29 | "rollup-plugin-uglify": "2.0.1", 30 | "rollup-watch": "4.3.1" 31 | }, 32 | "dependencies": { 33 | "preact": "^8.2.7", 34 | "preact-compat": "^3.18.0", 35 | "react-color": "https://github.com/casesandberg/react-color", 36 | "tinycolor2": "^1.4.1" 37 | }, 38 | "resolutions": { 39 | "prop-types": "15.6.0", 40 | "loose-envify": "1.3.1", 41 | "object-assign": "4.1.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | import alias from "rollup-plugin-alias"; 4 | import babel from "rollup-plugin-babel"; 5 | import cjs from "rollup-plugin-commonjs"; 6 | import globals from "rollup-plugin-node-globals"; 7 | import replace from "rollup-plugin-replace"; 8 | import resolve from "rollup-plugin-node-resolve"; 9 | import uglify from "rollup-plugin-uglify"; 10 | import browsersync from "rollup-plugin-browsersync"; 11 | 12 | const production = !process.env.ROLLUP_WATCH; 13 | 14 | const devPlugins = !production ? [browsersync({ server: "build" })] : []; 15 | const prodPlugins = production ? [uglify()] : []; 16 | 17 | export default { 18 | input: "src/index.js", 19 | output: { 20 | name: "colorpicker", 21 | file: production ? "build/app.min.js" : "build/app.js", 22 | format: "iife", 23 | sourcemap: true 24 | }, 25 | plugins: [ 26 | alias({ 27 | react: path.resolve( 28 | __dirname, 29 | "node_modules", 30 | "preact-compat", 31 | "dist", 32 | "preact-compat.es.js" 33 | ), 34 | "react-dom": path.resolve( 35 | __dirname, 36 | "node_modules", 37 | "preact-compat", 38 | "dist", 39 | "preact-compat.es.js" 40 | ), 41 | "create-react-class": path.resolve( 42 | __dirname, 43 | "node_modules", 44 | "preact-compat", 45 | "lib", 46 | "create-react-class.js" 47 | ) 48 | }), 49 | resolve({ 50 | browser: true, 51 | main: true 52 | }), 53 | cjs({ 54 | exclude: ["node_modules/process-es6/**", "node_modules/react-color/**"], 55 | include: [ 56 | "node_modules/fbjs/**", 57 | "node_modules/object-assign/**", 58 | "node_modules/reactcss/**", 59 | "node_modules/lodash/**", 60 | "node_modules/tinycolor2/**", 61 | "node_modules/prop-types/**" 62 | ], 63 | namedExports: { 64 | "node_modules/react/index.js": [ 65 | "Component", 66 | "PureComponent", 67 | "Children", 68 | "createElement" 69 | ] 70 | } 71 | }), 72 | globals(), 73 | replace({ 74 | "process.env.NODE_ENV": JSON.stringify( 75 | production ? "production" : "development" 76 | ) 77 | }), 78 | babel({ 79 | babelrc: false, 80 | presets: [ 81 | [ 82 | "env", 83 | { 84 | modules: false, 85 | loose: true 86 | // targets: { safari: 11 } 87 | } 88 | ], 89 | "stage-0", 90 | "react" 91 | ], 92 | plugins: ["external-helpers"] 93 | }), 94 | ...devPlugins, 95 | ...prodPlugins 96 | ] 97 | }; 98 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import SketchPicker from "react-color/src/components/sketch/Sketch"; 4 | import tinycolor from "tinycolor2"; 5 | 6 | function toState(input) { 7 | const color = tinycolor(input); 8 | 9 | return { 10 | format: color.getFormat() || "hex", 11 | color: color.toRgb() 12 | }; 13 | } 14 | 15 | export default class ColorPicker extends Component { 16 | state = toState(window.selectedColor); 17 | 18 | componentDidMount() { 19 | Object.defineProperty(window, "selectedColor", { 20 | get: () => { 21 | const color = tinycolor(this.state.color); 22 | 23 | switch (this.state.format) { 24 | case "hsl": 25 | return color.toHslString(); 26 | case "hex8": 27 | return color.toHex8String(); 28 | case "prgb": 29 | return color.toPercentageRgbString(); 30 | case "rgb": 31 | return color.toRgbString(); 32 | default: 33 | return color.getAlpha() === 1 34 | ? color.toHexString() 35 | : color.toRgbString(); 36 | } 37 | }, 38 | set: color => this.setState(toState(color)) 39 | }); 40 | } 41 | 42 | setColor = color => this.setState({ color: color.rgb }); 43 | 44 | render() { 45 | return ( 46 | 53 | ); 54 | } 55 | } 56 | 57 | ReactDOM.render(, document.getElementById("root")); 58 | -------------------------------------------------------------------------------- /client/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /color-picker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 |
12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /screenshots/webkit-color-picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozanmakes/emacs-webkit-color-picker/765cac80144cad4bc0bf59025ea0199f0486f737/screenshots/webkit-color-picker.gif -------------------------------------------------------------------------------- /webkit-color-picker.el: -------------------------------------------------------------------------------- 1 | ;;; webkit-color-picker.el --- Insert and adjust colors using Webkit Widgets -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2018 Ozan Sener 4 | 5 | ;; Author: Ozan Sener 6 | ;; URL: https://github.com/osener/emacs-webkit-color-picker 7 | ;; Maintainer: Ozan Sener 8 | ;; Version: 0.1.0 9 | ;; Keywords: tools 10 | ;; Package-Requires: ((emacs "26.0") (posframe "0.1.0")) 11 | 12 | ;; This file is NOT part of GNU Emacs. 13 | 14 | ;; The MIT License (MIT) 15 | 16 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 17 | ;; of this software and associated documentation files (the "Software"), to deal 18 | ;; in the Software without restriction, including without limitation the rights 19 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | ;; copies of the Software, and to permit persons to whom the Software is 21 | ;; furnished to do so, subject to the following conditions: 22 | 23 | ;; The above copyright notice and this permission notice shall be included in all 24 | ;; copies or substantial portions of the Software. 25 | 26 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | ;; SOFTWARE. 33 | 34 | ;;; Commentary: 35 | 36 | ;; #+OPTIONS: toc:nil title:nil timestamp:nil 37 | ;; * webkit-color-picker :README: 38 | 39 | ;; Small experiment with embedded a Webkit widgets in a childframe. Requires Emacs 26 compiled with [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Embedded-WebKit-Widgets.html][embedded Webkit Widget support]]. 40 | 41 | ;; webkit-color-picker is available on [[https://melpa.org/][MELPA]]. Example configuration using [[https://github.com/jwiegley/use-package][use-package]]: 42 | 43 | ;; #+BEGIN_SRC emacs-lisp 44 | ;; (use-package webkit-color-picker 45 | ;; :ensure t 46 | ;; :bind (("C-c C-p" . webkit-color-picker-show))) 47 | ;; #+END_SRC 48 | 49 | ;; ** Screenshot 50 | ;; [[./screenshots/webkit-color-picker.gif]] 51 | 52 | ;;; Code: 53 | ;; * webkit-color-picker's code 54 | 55 | (require 'xwidget) 56 | (require 'posframe) 57 | (require 'css-mode) 58 | (eval-when-compile (require 'subr-x)) 59 | (eval-when-compile (require 'cl-lib)) 60 | 61 | (defvar webkit-color-picker--client-path 62 | (concat "file://" 63 | (file-name-directory (or load-file-name buffer-file-name)) 64 | "color-picker.html")) 65 | 66 | (defun webkit-color-picker--run-xwidget () 67 | "Launch embedded Webkit instance." 68 | (with-current-buffer " *webkit-color-picker*" 69 | (let ((inhibit-read-only t)) 70 | (goto-char 1) 71 | 72 | (let ((id (make-xwidget 73 | 'webkit 74 | nil 75 | (window-pixel-width) 76 | (window-pixel-height) 77 | nil 78 | "*webkit-color-picker*"))) 79 | (put-text-property (point) (+ 1 (point)) 80 | 'display (list 'xwidget ':xwidget id)) 81 | (xwidget-webkit-mode) 82 | (xwidget-webkit-goto-uri (xwidget-at 1) 83 | webkit-color-picker--client-path))))) 84 | 85 | (defun webkit-color-picker--show () 86 | "Make color picker childframe visible." 87 | (when-let* ((current-frame (selected-frame)) 88 | (buffer (webkit-color-picker--get-buffer)) 89 | (frame (webkit-color-picker--get-frame))) 90 | (progn 91 | (select-frame frame t) 92 | (switch-to-buffer buffer t t) 93 | (select-frame current-frame t) 94 | (make-frame-visible frame) 95 | (redraw-frame frame) 96 | 97 | (let* 98 | ((position (point)) 99 | (parent-window (selected-window)) 100 | (parent-frame (window-frame parent-window)) 101 | (x-pixel-offset 0) 102 | (y-pixel-offset 0) 103 | (font-width (default-font-width)) 104 | (font-height (posframe--get-font-height position)) 105 | (frame-resize-pixelwise t) 106 | (position (posframe-poshandler-point-bottom-left-corner 107 | `(;All poshandlers will get info from this plist. 108 | :position ,position 109 | :font-height ,font-height 110 | :font-width ,font-width 111 | :posframe ,frame 112 | :posframe-buffer ,buffer 113 | :parent-frame ,parent-frame 114 | :parent-window ,parent-window 115 | :x-pixel-offset ,x-pixel-offset 116 | :y-pixel-offset ,y-pixel-offset)))) 117 | 118 | (set-frame-position frame (car position) (cdr position)))))) 119 | 120 | (defun webkit-color-picker--create () 121 | "Create a new posframe and launch Webkit." 122 | (let ((x-pointer-shape x-pointer-top-left-arrow)) 123 | (posframe-show " *webkit-color-picker*" 124 | :string " " 125 | :position (point))) 126 | 127 | (define-key (current-global-map) [xwidget-event] 128 | (lambda () 129 | (interactive) 130 | 131 | (let ((xwidget-event-type (nth 1 last-input-event))) 132 | (when (eq xwidget-event-type 'load-changed) 133 | (webkit-color-picker--resize) 134 | (webkit-color-picker--set-background)) 135 | 136 | (when (eq xwidget-event-type 'javascript-callback) 137 | (let ((proc (nth 3 last-input-event)) 138 | (arg (nth 4 last-input-event))) 139 | (funcall proc arg)))))) 140 | 141 | (webkit-color-picker--run-xwidget)) 142 | 143 | (defun webkit-color-picker--get-buffer () 144 | "Return color picker buffer." 145 | (get-buffer " *webkit-color-picker*")) 146 | 147 | (defun webkit-color-picker--get-frame () 148 | "Return color picker frame." 149 | (when-let* ((buffer (webkit-color-picker--get-buffer))) 150 | (seq-find 151 | (lambda (frame) 152 | (let ((buffer-info (frame-parameter frame 'posframe-buffer))) 153 | (or (eq buffer (car buffer-info)) 154 | (eq buffer (cdr buffer-info))))) 155 | (frame-list)))) 156 | 157 | (defun webkit-color-picker--set-background () 158 | "Evaluate JS code in color picker Webkit instance." 159 | (webkit-color-picker--execute-script 160 | (format "document.body.style.background = '%s';" 161 | (face-attribute 'default :background)))) 162 | 163 | (defun webkit-color-picker--insert-color () 164 | "Get the selected color from the widget and insert in the current buffer." 165 | (webkit-color-picker--execute-script 166 | "window.selectedColor;" 167 | `(lambda (color) 168 | (let ((color (kill-new (or color ""))) 169 | (start (or (car webkit-color-picker--last-position) (point))) 170 | (end (or (cdr webkit-color-picker--last-position) (point)))) 171 | (when (> (length color) 0) 172 | (delete-region start end) 173 | (goto-char start) 174 | (insert color) 175 | (webkit-color-picker-hide)))))) 176 | 177 | (defvar webkit-color-picker--emulation-alist '((t . nil))) 178 | 179 | (defvar-local webkit-color-picker--my-keymap nil) 180 | (defvar-local webkit-color-picker--last-position nil) 181 | 182 | (defsubst webkit-color-picker--enable-overriding-keymap (keymap) 183 | "Enable color picker overriding KEYMAP." 184 | (webkit-color-picker--uninstall-map) 185 | (setq webkit-color-picker--my-keymap keymap)) 186 | 187 | (defun webkit-color-picker--ensure-emulation-alist () 188 | "Append color picker emulation alist." 189 | (unless (eq 'webkit-color-picker--emulation-alist (car emulation-mode-map-alists)) 190 | (setq emulation-mode-map-alists 191 | (cons 'webkit-color-picker--emulation-alist 192 | (delq 'webkit-color-picker--emulation-alist emulation-mode-map-alists))))) 193 | 194 | ;; TODO: Find a better way of preventing accidental keystrokes whether the 195 | ;; childframe is in focus or not 196 | (defun webkit-color-picker--install-map () 197 | "Install temporary color picker keymap." 198 | (unless (or (cdar webkit-color-picker--emulation-alist) 199 | (null webkit-color-picker--my-keymap)) 200 | (setf (cdar webkit-color-picker--emulation-alist) webkit-color-picker--my-keymap))) 201 | 202 | (defun webkit-color-picker--uninstall-map () 203 | "Uninstall temporary color picker keymap." 204 | (setf (cdar webkit-color-picker--emulation-alist) nil)) 205 | 206 | (defvar webkit-color-picker--active-map 207 | (let ((keymap (make-sparse-keymap))) 208 | (define-key keymap "\e\e\e" 'webkit-color-picker-hide) 209 | (define-key keymap "\C-g" 'webkit-color-picker-hide) 210 | (define-key keymap [mouse-1] (lambda () (interactive) (webkit-color-picker--insert-color))) 211 | (define-key keymap (kbd "RET") (lambda () (interactive) (webkit-color-picker--insert-color))) 212 | keymap) 213 | "Keymap that is enabled during an active completion.") 214 | 215 | (defvar webkit-color-picker--hex-color-regexp 216 | (concat 217 | ;; Short hex. css-color-4 adds alpha. 218 | "\\(#[0-9a-fA-F]\\{3,4\\}\\b\\)" 219 | "\\|" 220 | ;; Long hex. css-color-4 adds alpha. 221 | "\\(#\\(?:[0-9a-fA-F][0-9a-fA-F]\\)\\{3,4\\}\\b\\)")) 222 | 223 | (defun webkit-color-picker--get-hex-color-at-point () 224 | "Return hex color at point." 225 | (with-syntax-table (copy-syntax-table (syntax-table)) 226 | (modify-syntax-entry ?# "w") ; Make `#' a word constituent. 227 | (when-let* ((word (thing-at-point 'word t)) 228 | (bounds (bounds-of-thing-at-point 'word))) 229 | (when (string-match webkit-color-picker--hex-color-regexp word) 230 | (cons word bounds))))) 231 | 232 | (defun webkit-color-picker--get-named-color-at-point () 233 | "Return color name at point." 234 | (when-let* ((word (word-at-point)) 235 | (color (assoc (downcase word) css--color-map))) 236 | (cons word (bounds-of-thing-at-point 'word)))) 237 | 238 | (defun webkit-color-picker--get-rgb-or-hsl-color-at-point () 239 | "Return RGB or HSL formatted color at point." 240 | (save-excursion 241 | (when-let* ((open-paren-pos (nth 1 (syntax-ppss)))) 242 | (when (save-excursion 243 | (goto-char open-paren-pos) 244 | (looking-back "\\(?:hsl\\|rgb\\)a?" (- (point) 4))) 245 | (goto-char (nth 1 (syntax-ppss))))) 246 | (when (eq (char-before) ?\)) 247 | (backward-sexp)) 248 | (skip-chars-backward "rgbhslaRGBHSLA") 249 | (when (looking-at "\\(\\_<\\(?:hsl\\|rgb\\)a?(\\)") 250 | (when-let* ((start (point)) 251 | (end (search-forward ")" nil t))) 252 | (cons (buffer-substring-no-properties start end) (cons start end)))))) 253 | 254 | (defun webkit-color-picker--color-at-point () 255 | "Return recognized color at point." 256 | (or 257 | (webkit-color-picker--get-rgb-or-hsl-color-at-point) 258 | (webkit-color-picker--get-named-color-at-point) 259 | (webkit-color-picker--get-hex-color-at-point))) 260 | 261 | (defun webkit-color-picker--get-xwidget () 262 | "Return Xwidget instance." 263 | (with-current-buffer " *webkit-color-picker*" 264 | (xwidget-at 1))) 265 | 266 | (defun webkit-color-picker--execute-script (script &optional fn) 267 | "Execute SCRIPT in embedded Xwidget and run optional callback FN." 268 | (when-let* ((xw (webkit-color-picker--get-xwidget))) 269 | (xwidget-webkit-execute-script xw script fn))) 270 | 271 | (defun webkit-color-picker--resize () 272 | "Resize color picker frame to widget boundaries." 273 | (webkit-color-picker--execute-script 274 | "[document.querySelector('.picker').offsetWidth, document.querySelector('.picker').offsetHeight];" 275 | (lambda (size) 276 | (when-let* ((frame (webkit-color-picker--get-frame))) 277 | (modify-frame-parameters 278 | frame 279 | `((width . (text-pixels . ,(+ 30 (aref size 0)))) 280 | (height . (text-pixels . ,(+ 30 (aref size 1)))) 281 | (inhibit-double-buffering . t))))))) 282 | 283 | (defun webkit-color-picker--set-color (color) 284 | "Update color picker widget state with COLOR." 285 | (webkit-color-picker--execute-script 286 | (format 287 | "window.selectedColor = '%s';" 288 | (if (stringp color) color "#000000")))) 289 | 290 | ;;;###autoload 291 | (defun webkit-color-picker-show () 292 | "Activate color picker." 293 | (interactive) 294 | (unless (featurep 'xwidget-internal) 295 | (user-error "Your Emacs was not compiled with xwidgets support")) 296 | (unless (display-graphic-p) 297 | (user-error "webkit-color-picker only works in graphical displays")) 298 | (let ((color-at-point (webkit-color-picker--color-at-point))) 299 | (if (buffer-live-p (webkit-color-picker--get-buffer)) 300 | (webkit-color-picker--show) 301 | (webkit-color-picker--create)) 302 | 303 | (webkit-color-picker--set-color (car color-at-point)) 304 | (webkit-color-picker--set-background) 305 | 306 | (setq-local webkit-color-picker--last-position 307 | (or (cdr color-at-point) 308 | (cons (point) (point)))) 309 | 310 | (webkit-color-picker--ensure-emulation-alist) 311 | (webkit-color-picker--enable-overriding-keymap webkit-color-picker--active-map) 312 | (webkit-color-picker--install-map) 313 | 314 | t)) 315 | 316 | ;;;###autoload 317 | (defun webkit-color-picker-hide () 318 | "Hide color picker frame." 319 | (interactive) 320 | (when-let* ((frame (webkit-color-picker--get-frame))) 321 | (make-frame-invisible frame)) 322 | (webkit-color-picker--enable-overriding-keymap nil)) 323 | 324 | ;;;###autoload 325 | (defun webkit-color-picker-cleanup () 326 | "Destroy color picker buffer and frame." 327 | (interactive) 328 | (dolist (xwidget-view xwidget-view-list) 329 | (delete-xwidget-view xwidget-view)) 330 | (posframe-delete-all) 331 | (kill-buffer " *webkit-color-picker*")) 332 | 333 | (provide 'webkit-color-picker) 334 | 335 | ;; Local Variables: 336 | ;; coding: utf-8-unix 337 | ;; End: 338 | 339 | ;;; webkit-color-picker.el ends here 340 | -------------------------------------------------------------------------------- /webkit-color-picker.org: -------------------------------------------------------------------------------- 1 | # Created 2018-03-25 Sun 08:55 2 | #+OPTIONS: toc:nil 3 | #+TITLE: Insert and adjust colors using Webkit Widgets 4 | #+AUTHOR: Ozan Sener 5 | Copyright (C) 2018 Ozan Sener 6 | 7 | Author: Ozan Sener 8 | URL: https://github.com/osener/emacs-webkit-color-picker 9 | Maintainer: Ozan Sener 10 | Version: 0.1.0 11 | Keywords: tools 12 | Package-Requires: ((emacs "26.0") (posframe "0.1.0")) 13 | 14 | This file is NOT part of GNU Emacs. 15 | 16 | The MIT License (MIT) 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | * webkit-color-picker :README: 36 | 37 | Small experiment with embedded a Webkit widgets in a childframe. Requires Emacs 26 compiled with [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Embedded-WebKit-Widgets.html][embedded Webkit Widget support]]. 38 | 39 | webkit-color-picker is available on [[https://melpa.org/][MELPA]]. 40 | 41 | #+BEGIN_EXAMPLE 42 | (use-package webkit-color-picker 43 | :ensure t 44 | :bind (("C-c C-p" . webkit-color-picker-show))) 45 | #+END_EXAMPLE 46 | 47 | ** Screenshot 48 | [[file:./screenshots/webkit-color-picker.gif]] 49 | * webkit-color-picker's code 50 | 51 | #+BEGIN_SRC emacs-lisp 52 | (require 'xwidget) 53 | (require 'posframe) 54 | (require 'css-mode) 55 | (eval-when-compile (require 'subr-x)) 56 | (eval-when-compile (require 'cl-lib)) 57 | #+END_SRC 58 | 59 | #+BEGIN_SRC emacs-lisp 60 | (defvar webkit-color-picker--client-path 61 | (concat "file://" 62 | (file-name-directory (or load-file-name buffer-file-name)) 63 | "color-picker.html")) 64 | #+END_SRC 65 | 66 | #+BEGIN_SRC emacs-lisp 67 | (defun webkit-color-picker--run-xwidget () 68 | "Launch embedded Webkit instance." 69 | (with-current-buffer " *webkit-color-picker*" 70 | (let ((inhibit-read-only t)) 71 | (goto-char 1) 72 | 73 | (let ((id (make-xwidget 74 | 'webkit 75 | nil 76 | (window-pixel-width) 77 | (window-pixel-height) 78 | nil 79 | "*webkit-color-picker*"))) 80 | (put-text-property (point) (+ 1 (point)) 81 | 'display (list 'xwidget ':xwidget id)) 82 | (xwidget-webkit-mode) 83 | (xwidget-webkit-goto-uri (xwidget-at 1) 84 | webkit-color-picker--client-path))))) 85 | #+END_SRC 86 | 87 | #+BEGIN_SRC emacs-lisp 88 | (defun webkit-color-picker--show () 89 | "Make color picker childframe visible." 90 | (when-let* ((current-frame (selected-frame)) 91 | (buffer (webkit-color-picker--get-buffer)) 92 | (frame (webkit-color-picker--get-frame))) 93 | (progn 94 | (select-frame frame t) 95 | (switch-to-buffer buffer t t) 96 | (select-frame current-frame t) 97 | (make-frame-visible frame) 98 | (redraw-frame frame) 99 | 100 | (let* 101 | ((position (point)) 102 | (parent-window (selected-window)) 103 | (parent-frame (window-frame parent-window)) 104 | (x-pixel-offset 0) 105 | (y-pixel-offset 0) 106 | (font-width (default-font-width)) 107 | (font-height (posframe--get-font-height position)) 108 | (frame-resize-pixelwise t) 109 | (position (posframe-poshandler-point-bottom-left-corner 110 | `(;All poshandlers will get info from this plist. 111 | :position ,position 112 | :font-height ,font-height 113 | :font-width ,font-width 114 | :posframe ,frame 115 | :posframe-buffer ,buffer 116 | :parent-frame ,parent-frame 117 | :parent-window ,parent-window 118 | :x-pixel-offset ,x-pixel-offset 119 | :y-pixel-offset ,y-pixel-offset)))) 120 | 121 | (set-frame-position frame (car position) (cdr position)))))) 122 | #+END_SRC 123 | 124 | #+BEGIN_SRC emacs-lisp 125 | (defun webkit-color-picker--create () 126 | "Create a new posframe and launch Webkit." 127 | (let ((x-pointer-shape x-pointer-top-left-arrow)) 128 | (posframe-show " *webkit-color-picker*" 129 | :string " " 130 | :position (point))) 131 | 132 | (define-key (current-global-map) [xwidget-event] 133 | (lambda () 134 | (interactive) 135 | 136 | (let ((xwidget-event-type (nth 1 last-input-event))) 137 | (when (eq xwidget-event-type 'load-changed) 138 | (webkit-color-picker--resize) 139 | (webkit-color-picker--set-background)) 140 | 141 | (when (eq xwidget-event-type 'javascript-callback) 142 | (let ((proc (nth 3 last-input-event)) 143 | (arg (nth 4 last-input-event))) 144 | (funcall proc arg)))))) 145 | 146 | (webkit-color-picker--run-xwidget)) 147 | #+END_SRC 148 | 149 | #+BEGIN_SRC emacs-lisp 150 | (defun webkit-color-picker--get-buffer () 151 | "Return color picker buffer." 152 | (get-buffer " *webkit-color-picker*")) 153 | #+END_SRC 154 | 155 | #+BEGIN_SRC emacs-lisp 156 | (defun webkit-color-picker--get-frame () 157 | "Return color picker frame." 158 | (when-let* ((buffer (webkit-color-picker--get-buffer))) 159 | (seq-find 160 | (lambda (frame) 161 | (let ((buffer-info (frame-parameter frame 'posframe-buffer))) 162 | (or (eq buffer (car buffer-info)) 163 | (eq buffer (cdr buffer-info))))) 164 | (frame-list)))) 165 | #+END_SRC 166 | 167 | #+BEGIN_SRC emacs-lisp 168 | (defun webkit-color-picker--set-background () 169 | "Evaluate JS code in color picker Webkit instance." 170 | (webkit-color-picker--execute-script 171 | (format "document.body.style.background = '%s';" 172 | (face-attribute 'default :background)))) 173 | #+END_SRC 174 | 175 | #+BEGIN_SRC emacs-lisp 176 | (defun webkit-color-picker--insert-color () 177 | "Get the selected color from the widget and insert in the current buffer." 178 | (webkit-color-picker--execute-script 179 | "window.selectedColor;" 180 | `(lambda (color) 181 | (let ((color (kill-new (or color ""))) 182 | (start (or (car webkit-color-picker--last-position) (point))) 183 | (end (or (cdr webkit-color-picker--last-position) (point)))) 184 | (when (> (length color) 0) 185 | (delete-region start end) 186 | (goto-char start) 187 | (insert color) 188 | (webkit-color-picker-hide)))))) 189 | #+END_SRC 190 | 191 | #+BEGIN_SRC emacs-lisp 192 | (defvar webkit-color-picker--emulation-alist '((t . nil))) 193 | #+END_SRC 194 | 195 | #+BEGIN_SRC emacs-lisp 196 | (defvar-local webkit-color-picker--my-keymap nil) 197 | (defvar-local webkit-color-picker--last-position nil) 198 | #+END_SRC 199 | 200 | #+BEGIN_SRC emacs-lisp 201 | (defsubst webkit-color-picker--enable-overriding-keymap (keymap) 202 | "Enable color picker overriding KEYMAP." 203 | (webkit-color-picker--uninstall-map) 204 | (setq webkit-color-picker--my-keymap keymap)) 205 | #+END_SRC 206 | 207 | #+BEGIN_SRC emacs-lisp 208 | (defun webkit-color-picker--ensure-emulation-alist () 209 | "Append color picker emulation alist." 210 | (unless (eq 'webkit-color-picker--emulation-alist (car emulation-mode-map-alists)) 211 | (setq emulation-mode-map-alists 212 | (cons 'webkit-color-picker--emulation-alist 213 | (delq 'webkit-color-picker--emulation-alist emulation-mode-map-alists))))) 214 | #+END_SRC 215 | 216 | TODO: Find a better way of preventing accidental keystrokes whether the 217 | childframe is in focus or not 218 | #+BEGIN_SRC emacs-lisp 219 | (defun webkit-color-picker--install-map () 220 | "Install temporary color picker keymap." 221 | (unless (or (cdar webkit-color-picker--emulation-alist) 222 | (null webkit-color-picker--my-keymap)) 223 | (setf (cdar webkit-color-picker--emulation-alist) webkit-color-picker--my-keymap))) 224 | #+END_SRC 225 | 226 | #+BEGIN_SRC emacs-lisp 227 | (defun webkit-color-picker--uninstall-map () 228 | "Uninstall temporary color picker keymap." 229 | (setf (cdar webkit-color-picker--emulation-alist) nil)) 230 | #+END_SRC 231 | 232 | #+BEGIN_SRC emacs-lisp 233 | (defvar webkit-color-picker--active-map 234 | (let ((keymap (make-sparse-keymap))) 235 | (define-key keymap "\e\e\e" 'webkit-color-picker-hide) 236 | (define-key keymap "\C-g" 'webkit-color-picker-hide) 237 | (define-key keymap [mouse-1] (lambda () (interactive) (webkit-color-picker--insert-color))) 238 | (define-key keymap (kbd "RET") (lambda () (interactive) (webkit-color-picker--insert-color))) 239 | keymap) 240 | "Keymap that is enabled during an active completion.") 241 | #+END_SRC 242 | 243 | #+BEGIN_SRC emacs-lisp 244 | (defvar webkit-color-picker--hex-color-regexp 245 | (concat 246 | ;; Short hex. css-color-4 adds alpha. 247 | "\\(#[0-9a-fA-F]\\{3,4\\}\\b\\)" 248 | "\\|" 249 | ;; Long hex. css-color-4 adds alpha. 250 | "\\(#\\(?:[0-9a-fA-F][0-9a-fA-F]\\)\\{3,4\\}\\b\\)")) 251 | #+END_SRC 252 | 253 | #+BEGIN_SRC emacs-lisp 254 | (defun webkit-color-picker--get-hex-color-at-point () 255 | "Return hex color at point." 256 | (with-syntax-table (copy-syntax-table (syntax-table)) 257 | (modify-syntax-entry ?# "w") ; Make `#' a word constituent. 258 | (when-let* ((word (thing-at-point 'word t)) 259 | (bounds (bounds-of-thing-at-point 'word))) 260 | (when (string-match webkit-color-picker--hex-color-regexp word) 261 | (cons word bounds))))) 262 | #+END_SRC 263 | 264 | #+BEGIN_SRC emacs-lisp 265 | (defun webkit-color-picker--get-named-color-at-point () 266 | "Return color name at point." 267 | (when-let* ((word (word-at-point)) 268 | (color (assoc (downcase word) css--color-map))) 269 | (cons word (bounds-of-thing-at-point 'word)))) 270 | #+END_SRC 271 | 272 | #+BEGIN_SRC emacs-lisp 273 | (defun webkit-color-picker--get-rgb-or-hsl-color-at-point () 274 | "Return RGB or HSL formatted color at point." 275 | (save-excursion 276 | (when-let* ((open-paren-pos (nth 1 (syntax-ppss)))) 277 | (when (save-excursion 278 | (goto-char open-paren-pos) 279 | (looking-back "\\(?:hsl\\|rgb\\)a?" (- (point) 4))) 280 | (goto-char (nth 1 (syntax-ppss))))) 281 | (when (eq (char-before) ?\)) 282 | (backward-sexp)) 283 | (skip-chars-backward "rgbhslaRGBHSLA") 284 | (when (looking-at "\\(\\_<\\(?:hsl\\|rgb\\)a?(\\)") 285 | (when-let* ((start (point)) 286 | (end (search-forward ")" nil t))) 287 | (cons (buffer-substring-no-properties start end) (cons start end)))))) 288 | #+END_SRC 289 | 290 | #+BEGIN_SRC emacs-lisp 291 | (defun webkit-color-picker--color-at-point () 292 | "Return recognized color at point." 293 | (or 294 | (webkit-color-picker--get-rgb-or-hsl-color-at-point) 295 | (webkit-color-picker--get-named-color-at-point) 296 | (webkit-color-picker--get-hex-color-at-point))) 297 | #+END_SRC 298 | 299 | #+BEGIN_SRC emacs-lisp 300 | (defun webkit-color-picker--get-xwidget () 301 | "Return Xwidget instance." 302 | (with-current-buffer " *webkit-color-picker*" 303 | (xwidget-at 1))) 304 | #+END_SRC 305 | 306 | #+BEGIN_SRC emacs-lisp 307 | (defun webkit-color-picker--execute-script (script &optional fn) 308 | "Execute SCRIPT in embedded Xwidget and run optional callback FN." 309 | (when-let* ((xw (webkit-color-picker--get-xwidget))) 310 | (xwidget-webkit-execute-script xw script fn))) 311 | #+END_SRC 312 | 313 | #+BEGIN_SRC emacs-lisp 314 | (defun webkit-color-picker--resize () 315 | "Resize color picker frame to widget boundaries." 316 | (webkit-color-picker--execute-script 317 | "[document.querySelector('.picker').offsetWidth, document.querySelector('.picker').offsetHeight];" 318 | (lambda (size) 319 | (when-let* ((frame (webkit-color-picker--get-frame))) 320 | (modify-frame-parameters 321 | frame 322 | `((width . (text-pixels . ,(+ 30 (aref size 0)))) 323 | (height . (text-pixels . ,(+ 30 (aref size 1)))) 324 | (inhibit-double-buffering . t))))))) 325 | #+END_SRC 326 | 327 | #+BEGIN_SRC emacs-lisp 328 | (defun webkit-color-picker--set-color (color) 329 | "Update color picker widget state with COLOR." 330 | (webkit-color-picker--execute-script 331 | (format 332 | "window.selectedColor = '%s';" 333 | (if (stringp color) color "#000000")))) 334 | #+END_SRC 335 | 336 | #+BEGIN_SRC emacs-lisp 337 | (defun webkit-color-picker-show () 338 | "Activate color picker." 339 | (interactive) 340 | (or (featurep 'xwidget-internal) 341 | (user-error "Your Emacs was not compiled with xwidgets support")) 342 | (let ((color-at-point (webkit-color-picker--color-at-point))) 343 | (if (buffer-live-p (webkit-color-picker--get-buffer)) 344 | (webkit-color-picker--show) 345 | (webkit-color-picker--create)) 346 | 347 | (webkit-color-picker--set-color (car color-at-point)) 348 | (webkit-color-picker--set-background) 349 | 350 | (setq-local webkit-color-picker--last-position 351 | (or (cdr color-at-point) 352 | (cons (point) (point)))) 353 | 354 | (webkit-color-picker--ensure-emulation-alist) 355 | (webkit-color-picker--enable-overriding-keymap webkit-color-picker--active-map) 356 | (webkit-color-picker--install-map) 357 | 358 | t)) 359 | #+END_SRC 360 | 361 | #+BEGIN_SRC emacs-lisp 362 | (defun webkit-color-picker-hide () 363 | "Hide color picker frame." 364 | (interactive) 365 | (when-let* ((frame (webkit-color-picker--get-frame))) 366 | (make-frame-invisible frame)) 367 | (webkit-color-picker--enable-overriding-keymap nil)) 368 | #+END_SRC 369 | 370 | #+BEGIN_SRC emacs-lisp 371 | (defun webkit-color-picker-cleanup () 372 | "Destroy color picker buffer and frame." 373 | (interactive) 374 | (dolist (xwidget-view xwidget-view-list) 375 | (delete-xwidget-view xwidget-view)) 376 | (posframe-delete-all) 377 | (kill-buffer " *webkit-color-picker*")) 378 | #+END_SRC 379 | 380 | #+BEGIN_SRC emacs-lisp 381 | (provide 'webkit-color-picker) 382 | #+END_SRC 383 | 384 | #+BEGIN_EXAMPLE 385 | Local Variables: 386 | coding: utf-8-unix 387 | End: 388 | #+END_EXAMPLE 389 | --------------------------------------------------------------------------------