diff options
Diffstat (limited to 'lisp/emacs-lisp/icons.el')
-rw-r--r-- | lisp/emacs-lisp/icons.el | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/lisp/emacs-lisp/icons.el b/lisp/emacs-lisp/icons.el new file mode 100644 index 00000000000..277b285c2ef --- /dev/null +++ b/lisp/emacs-lisp/icons.el @@ -0,0 +1,265 @@ +;;; icons.el --- Handling icons -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Lars Ingebrigtsen <larsi@gnus.org> +;; Keywords: icons buttons + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'cl-lib) + +(defface icon + '((t :underline nil)) + "Face for buttons." + :version "29.1" + :group 'customize) + +(defface icon-button + '((((type x w32 ns haiku pgtk) (class color)) + :inherit icon + :box (:line-width (3 . -1) :color "#404040" :style flat-button) + :background "#808080" + :foreground "black")) + "Face for buttons." + :version "29.1" + :group 'customize) + +(defcustom icon-preference '(image emoji symbol text) + "List of icon types to use, in order of preference. +Emacs will choose the icon of the highest preference possible +on the current display, and \"degrade\" gracefully to an icon +type that's available." + :version "29.1" + :group 'customize + :type '(repeat (choice (const :tag "Images" image) + (const :tag "Colorful Emojis" emoji) + (const :tag "Monochrome Symbols" symbol) + (const :tag "Text Only" text)))) + +(defmacro define-icon (name parent specification documentation &rest keywords) + "Define an icon identified by NAME. +If non-nil, inherit the specification from PARENT. Entries from +SPECIFICATION will override inherited specifications. + +SPECIFICATION is an alist of entries where the first element is +the type, and the rest are icons of that type. Valid types are +`image', `emoji', `symbol' and `text'. + +KEYWORDS specify additional information. Valid keywords are: + +`:version': The first Emacs version to include this icon; this is +mandatory. + +`:group': The customization group the icon belongs in; this is +inferred if not present. + +`:help-echo': Informational text that explains what happens if +the icon is used as a button and you click it." + (declare (indent 2)) + (unless (symbolp name) + (error "NAME must be a symbol: %S" name)) + (unless (plist-get keywords :version) + (error "There must be a :version keyword in `define-icon'")) + `(icons--register ',name ',parent ,specification ,documentation + ',keywords)) + +(defun icons--register (name parent spec doc keywords) + (put name 'icon--properties (list parent spec doc keywords)) + (custom-add-to-group + (or (plist-get keywords :group) + (custom-current-group)) + name 'custom-icon)) + +(defun icon-spec-keywords (spec) + (seq-drop-while (lambda (e) (not (keywordp e))) (cdr spec))) + +(defun icon-spec-values (spec) + (seq-take-while (lambda (e) (not (keywordp e))) (cdr spec))) + +(defun iconp (object) + "Return nil if OBJECT is not an icon. +If OBJECT is an icon, return the icon properties." + (get object 'icon--properties)) + +(defun icon-documentation (icon) + "Return the documentation for ICON." + (let ((props (iconp icon))) + (unless props + (user-error "%s is not a valid icon" icon)) + (nth 2 props))) + +(defun icons--spec (icon) + (nth 1 (iconp icon))) + +(defun icons--copy-spec (spec) + (mapcar #'copy-sequence spec)) + +(defun icon-complete-spec (icon &optional inhibit-theme inhibit-inheritance) + "Return the merged spec for ICON." + (pcase-let ((`(,parent ,spec _ _) (iconp icon))) + ;; We destructively modify `spec' when merging, so copy it. + (setq spec (icons--copy-spec spec)) + ;; Let the Customize theme override. + (unless inhibit-theme + (when-let ((theme-spec (cadr (car (get icon 'theme-icon))))) + (setq spec (icons--merge-spec (icons--copy-spec theme-spec) spec)))) + ;; Inherit from the parent spec (recursively). + (unless inhibit-inheritance + (while parent + (let ((parent-props (get parent 'icon--properties))) + (when parent-props + (setq spec (icons--merge-spec spec (cadr parent-props)))) + (setq parent (car parent-props))))) + spec)) + +(defun icon-string (name) + "Return a string suitable for display in the current buffer for icon NAME." + (let ((props (iconp name))) + (unless props + (user-error "%s is not a valid icon" name)) + (pcase-let ((`(_ ,spec _ ,keywords) props)) + (setq spec (icon-complete-spec name)) + ;; We now have a full spec, so check the intersection of what + ;; the user wants and what this Emacs is capable of showing. + (let ((icon-string + (catch 'found + (dolist (type icon-preference) + (let* ((type-spec (assq type spec)) + ;; Find the keywords at the end of the section + ;; (if any). + (type-keywords (icon-spec-keywords type-spec))) + ;; Go through all the variations in this section + ;; and return the first one we can display. + (dolist (icon (icon-spec-values type-spec)) + (when-let ((result + (icons--create type icon type-keywords))) + (throw 'found + (if-let ((face (plist-get type-keywords :face))) + (propertize result 'face face) + result))))))))) + (unless icon-string + (error "Couldn't find any way to display the %s icon" name)) + (when-let ((help (plist-get keywords :help-echo))) + (setq icon-string (propertize icon-string 'help-echo help))) + (propertize icon-string 'rear-nonsticky t))))) + +(defun icon-elements (name) + "Return the elements of icon NAME. +The elements are represented as a plist where the keys are +`string', `face' and `display'. The `image' element is only +present if the icon is represented by an image." + (let ((string (icon-string name))) + (list 'face (get-text-property 0 'face string) + 'image (get-text-property 0 'display string) + 'string (substring-no-properties string)))) + +(defun icons--merge-spec (merged parent-spec) + (dolist (elem parent-spec) + (let ((current (assq (car elem) merged))) + (if (not current) + ;; Just add the entry. + (push elem merged) + ;; See if there are any keywords to inherit. + (let ((parent-keywords (icon-spec-keywords elem)) + (current-keywords (icon-spec-keywords current))) + (while parent-keywords + (unless (plist-get (car parent-keywords) current-keywords) + (nconc current (take 2 parent-keywords)) + (setq parent-keywords (cddr parent-keywords)))))))) + merged) + +(cl-defmethod icons--create ((_type (eql 'image)) icon keywords) + (let ((file (if (file-name-absolute-p icon) + icon + (image-search-load-path icon)))) + (and (display-images-p) + (image-supported-file-p file) + (propertize + " " 'display + (if-let ((height (plist-get keywords :height))) + (create-image file + nil nil + :height (if (eq height 'line) + (window-default-line-height) + height) + :scale 1) + (create-image file)))))) + +(cl-defmethod icons--create ((_type (eql 'emoji)) icon _keywords) + (when-let ((font (and (display-multi-font-p) + ;; FIXME: This is not enough for ensuring + ;; display of color Emoji. + (car (internal-char-font nil ?🟠))))) + (and (font-has-char-p font (aref icon 0)) + icon))) + +(cl-defmethod icons--create ((_type (eql 'symbol)) icon _keywords) + (and (cl-every #'char-displayable-p icon) + icon)) + +(cl-defmethod icons--create ((_type (eql 'text)) icon _keywords) + icon) + +(define-icon button nil + '((image :face icon-button) + (emoji "🔵" :face icon) + (symbol "●" :face icon-button) + (text "button" :face icon-button)) + "Base icon for buttons." + :version "29.1") + +;;;###autoload +(defun describe-icon (icon) + "Pop to a buffer to describe ICON." + (interactive + (list (intern (completing-read "Describe icon: " obarray 'iconp t)))) + (let ((help-buffer-under-preparation t)) + (help-setup-xref (list #'describe-icon icon) + (called-interactively-p 'interactive)) + (with-help-window (help-buffer) + (with-current-buffer standard-output + (insert "Icon: " (symbol-name icon) "\n\n") + (insert "Documentation:\n" + (substitute-command-keys (icon-documentation icon))) + (ensure-empty-lines) + (let ((spec (icon-complete-spec icon)) + (plain (icon-complete-spec icon t t))) + (insert "Specification including inheritance and theming:\n") + (icons--describe-spec spec) + (unless (equal spec plain) + (insert "\nSpecification not including inheritance and theming:\n") + (icons--describe-spec plain))))))) + +(defun icons--describe-spec (spec) + (dolist (elem spec) + (let ((type (car elem)) + (values (icon-spec-values elem)) + (keywords (icon-spec-keywords elem))) + (when (or values keywords) + (insert (format "\nType: %s\n" type)) + (dolist (value values) + (insert (format " %s\n" value))) + (while keywords + (insert (format " %s: %s\n" (pop keywords) (pop keywords)))))))) + +(provide 'icons) + +;;; icons.el ends here |