1045 lines
42 KiB
EmacsLisp
1045 lines
42 KiB
EmacsLisp
|
|
;;; svg-lib.el --- SVG tags, progress bars & icons -*- lexical-binding: t -*-
|
||
|
|
|
||
|
|
;; Copyright (C) 2021-2023 Free Software Foundation, Inc.
|
||
|
|
|
||
|
|
;; Maintainer: Nicolas P. Rougier <Nicolas.Rougier@inria.fr>
|
||
|
|
;; URL: https://github.com/rougier/svg-lib
|
||
|
|
;; Version: 0.3
|
||
|
|
;; Package-Requires: ((emacs "27.1"))
|
||
|
|
;; Keywords: svg, icons, tags, convenience
|
||
|
|
|
||
|
|
;; This file is not part of GNU Emacs.
|
||
|
|
|
||
|
|
;; This file 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, or (at your option)
|
||
|
|
;; any later version.
|
||
|
|
|
||
|
|
;; This file 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.
|
||
|
|
|
||
|
|
;; For a full copy of the GNU General Public License
|
||
|
|
;; see <https://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
;;; Commentary:
|
||
|
|
;;
|
||
|
|
;; Usage example:
|
||
|
|
;;
|
||
|
|
;; (insert-image (svg-lib-tag "TODO"))
|
||
|
|
;; (insert-image (svg-lib-progress-bar 0.33))
|
||
|
|
;; (insert-image (svg-lib-icon "star"))
|
||
|
|
;;
|
||
|
|
;; Icons ares created by parsing remote collections whose license are
|
||
|
|
;; compatibles with GNU Emacs:
|
||
|
|
;;
|
||
|
|
;; - Boxicons (https://github.com/atisawd/boxicons), available under a
|
||
|
|
;; Creative Commons 4.0 license. As of version 2.07 (December 2020),
|
||
|
|
;; this collection offers 1500 icons in two styles (regular & solid).
|
||
|
|
;; Gallery is available at https://boxicons.com/
|
||
|
|
;;
|
||
|
|
;; - Octicons (https://github.com/primer/octicons), available under a
|
||
|
|
;; MIT License with some usage restriction for the GitHub logo. As of
|
||
|
|
;; version 11.2.0 (December 2020), this collection offers 201 icons.
|
||
|
|
;; Gallery available at https://primer.style/octicons/
|
||
|
|
;;
|
||
|
|
;; - Material (https://github.com/google/material-design-icons),
|
||
|
|
;; available under an Apache 2.0 license. As of version 4.0.0
|
||
|
|
;; (December 2020), this collection offers 500+ icons in 4 styles
|
||
|
|
;; (filled, outlined, rounded, sharp). Gallery available at
|
||
|
|
;; https://material.io/resources/icons/?style=baseline
|
||
|
|
;;
|
||
|
|
;; - Bootstrap (https://github.com/twbs/icons), available under a MIT
|
||
|
|
;; license. As of version 1.2.1 (December 2020), this collection
|
||
|
|
;; offers 1200+ icons in 2 styles (regular & filled). Gallery
|
||
|
|
;; available at https://icons.getbootstrap.com/
|
||
|
|
;;
|
||
|
|
;; The default size of an icon is exactly 2x1 characters such that it
|
||
|
|
;; can be inserted inside a text without disturbing alignment.
|
||
|
|
;;
|
||
|
|
;; Note: Each icon is cached locally to speed-up loading the next time
|
||
|
|
;; you use it. If for some reason the cache is corrupted you can
|
||
|
|
;; force reload using the svg-icon-get-data function.
|
||
|
|
;;
|
||
|
|
;; If you want to add new collections (i.e. URL), make sure the icons
|
||
|
|
;; are monochrome and that their size is consistent.
|
||
|
|
|
||
|
|
;;; NEWS:
|
||
|
|
|
||
|
|
;; Version 0.3
|
||
|
|
;; - Renamed 'svg-lib-button' to 'svg-lib-icon+tag'
|
||
|
|
;; - Added interactive 'svg-lib-button' with associtated 'svg-lib-button-mode'
|
||
|
|
;; - Added proper documentation in the README
|
||
|
|
|
||
|
|
;; Version 0.2.8
|
||
|
|
;; - No background for icon when background color is nil
|
||
|
|
;; - Refactored date icons
|
||
|
|
|
||
|
|
;; Version 0.2.7
|
||
|
|
;; - Added a dynamic date icon
|
||
|
|
|
||
|
|
;; Version 0.2.6
|
||
|
|
;; - Bug fix with bootstrap icon directory
|
||
|
|
|
||
|
|
;; Version 0.2.5
|
||
|
|
;; - Bug fix in text size computation
|
||
|
|
|
||
|
|
;; Version 0.2.4
|
||
|
|
;; - Better error handling if SVG support is missing
|
||
|
|
|
||
|
|
;; Version 0.2.2
|
||
|
|
;; - Added a left/righ crop style argument to allow for tags collage.
|
||
|
|
|
||
|
|
;; Version 0.2.1
|
||
|
|
;; - Added an alignment parameter for moving tags inside margins.
|
||
|
|
|
||
|
|
;; Version 0.2
|
||
|
|
;; - Fix most of the warnings.
|
||
|
|
|
||
|
|
;; Version 0.1:
|
||
|
|
;; - Submission to ELPA
|
||
|
|
|
||
|
|
|
||
|
|
;;; Code:
|
||
|
|
(require 'svg)
|
||
|
|
(require 'xml)
|
||
|
|
(require 'cl-lib)
|
||
|
|
(require 'color)
|
||
|
|
|
||
|
|
;; Check if Emacs has been compiled with svg support
|
||
|
|
(defun svg-lib--image (&rest args)
|
||
|
|
;; FIXME: Should `svg-image' perform this check instead?
|
||
|
|
(unless (image-type-available-p 'svg)
|
||
|
|
(error "svg-lib.el requires Emacs to be compiled with svg support.\n"))
|
||
|
|
(apply #'svg-image args))
|
||
|
|
|
||
|
|
(defgroup svg-lib nil
|
||
|
|
"SVG tags, bars & icons."
|
||
|
|
:group 'convenience
|
||
|
|
:prefix "svg-lib-")
|
||
|
|
|
||
|
|
(defface svg-lib-button-active-face
|
||
|
|
`((t :foreground ,(face-foreground 'default)
|
||
|
|
:background ,(face-background 'default)
|
||
|
|
:family "RobotoMono Nerd Font"
|
||
|
|
:weight regular
|
||
|
|
:box (:line-width 2 :style nil)))
|
||
|
|
"Default face for active button"
|
||
|
|
:group 'svg-lib)
|
||
|
|
|
||
|
|
(defface svg-lib-button-hover-face
|
||
|
|
`((t :foreground ,(face-background 'font-lock-comment-face nil 'default)
|
||
|
|
:background ,(face-foreground 'font-lock-comment-face nil 'default)
|
||
|
|
:family "RobotoMono Nerd Font"
|
||
|
|
:weight semibold
|
||
|
|
:box nil))
|
||
|
|
"default face for when mouse is over the button"
|
||
|
|
:group 'svg-lib)
|
||
|
|
|
||
|
|
(defface svg-lib-button-press-face
|
||
|
|
`((t :foreground ,(face-background 'default)
|
||
|
|
:background ,(face-foreground 'default)
|
||
|
|
:family "RobotoMono Nerd Font"
|
||
|
|
:weight semibold
|
||
|
|
:box nil))
|
||
|
|
"Default face for when button is prssed (mouse click or keyboard)"
|
||
|
|
:group 'svg-lib)
|
||
|
|
|
||
|
|
;; Default icon collections
|
||
|
|
;; ---------------------------------------------------------------------
|
||
|
|
(defcustom svg-lib-icon-collections
|
||
|
|
'(("bootstrap" .
|
||
|
|
"https://icons.getbootstrap.com/assets/icons/%s.svg")
|
||
|
|
("simple" .
|
||
|
|
"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/%s.svg")
|
||
|
|
("material" .
|
||
|
|
"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/%s.svg")
|
||
|
|
("octicons" .
|
||
|
|
"https://raw.githubusercontent.com/primer/octicons/main/icons/%s-24.svg")
|
||
|
|
("boxicons" .
|
||
|
|
"https://boxicons.com/static/img/svg/regular/bx-%s.svg")
|
||
|
|
("vscode" .
|
||
|
|
"https://raw.githubusercontent.com/microsoft/vscode-icons/main/icons/light/%s.svg"))
|
||
|
|
|
||
|
|
"Various icons collections stored as (name . base-url).
|
||
|
|
|
||
|
|
The name of the collection is used as a pointer for the various
|
||
|
|
icon creation methods. The base-url is a string containing a %s
|
||
|
|
such that is can be replaced with the name of a specific icon.
|
||
|
|
User is responsible for finding/giving proper names for a given
|
||
|
|
collection (there are way too many to store them)."
|
||
|
|
|
||
|
|
:type '(alist :key-type (string :tag "Name")
|
||
|
|
:value-type (string :tag "URL"))
|
||
|
|
:group 'svg-lib)
|
||
|
|
|
||
|
|
(defcustom svg-lib-icons-dir
|
||
|
|
(expand-file-name (concat user-emacs-directory ".cache/svg-lib/"))
|
||
|
|
"svg-lib icons directory."
|
||
|
|
:group 'svg-lib
|
||
|
|
:type 'directory)
|
||
|
|
|
||
|
|
|
||
|
|
;; Default style for all objects
|
||
|
|
(defun svg-lib-style-compute-default (&optional face)
|
||
|
|
"Compute the default style according to face (which defaults
|
||
|
|
to the default face)."
|
||
|
|
|
||
|
|
(let* ((face (or face 'default))
|
||
|
|
(font-family (face-attribute face :family nil 'default))
|
||
|
|
(font-weight (face-attribute face :weight nil 'default))
|
||
|
|
(font-size (face-attribute face :height nil 'default))
|
||
|
|
(font-size (round (* font-size 0.085)))
|
||
|
|
(foreground (face-attribute face :foreground nil 'default))
|
||
|
|
(background (face-attribute face :background nil 'default)))
|
||
|
|
|
||
|
|
`(:background ,background
|
||
|
|
:foreground ,foreground
|
||
|
|
|
||
|
|
:padding 1 ;; In characters (tag and icons) or pixels (progress)
|
||
|
|
:margin 0 ;; In characters
|
||
|
|
:stroke 2 ;; In pixels
|
||
|
|
:radius 3 ;; In pixels
|
||
|
|
:alignment 0.5 ;; Horizontal alignment (in fraction of margin)
|
||
|
|
:width 20 ;; In characters
|
||
|
|
:height 0.90 ;; Ratio of text line height
|
||
|
|
:scale 0.75 ;; Icon scaling
|
||
|
|
:ascent center ;; Position / baseline
|
||
|
|
:crop-left nil ;; Whether to crop on left (for collage with other tags)
|
||
|
|
:crop-right nil ;; Whether to crop on righ (for collage with other tags)
|
||
|
|
|
||
|
|
:collection "material" ;; Icon collection
|
||
|
|
|
||
|
|
:font-family ,font-family
|
||
|
|
:font-size ,font-size
|
||
|
|
:font-weight ,font-weight)))
|
||
|
|
|
||
|
|
(defcustom svg-lib-style-default
|
||
|
|
(svg-lib-style-compute-default)
|
||
|
|
"Default style"
|
||
|
|
:type '(plist :key-type (choice (const :tag "Background" :background)
|
||
|
|
(const :tag "Foreground" :foreground)
|
||
|
|
(const :tag "Padding" :padding)
|
||
|
|
(const :tag "Margin" :margin)
|
||
|
|
(const :tag "Stroke" :stroke)
|
||
|
|
(const :tag "Radius" :radius)
|
||
|
|
(const :tag "Ascent" :ascent)
|
||
|
|
(const :tag "Alignment" :alignment)
|
||
|
|
(const :tag "Width" :width)
|
||
|
|
(const :tag "Height" :height)
|
||
|
|
(const :tag "Scale" :scale)
|
||
|
|
(const :tag "Crop Left" :crop-left)
|
||
|
|
(const :tag "Crop Right" :crop-right)
|
||
|
|
(const :tag "Collection" :collection)
|
||
|
|
(const :tag "Font Family" :font-family)
|
||
|
|
(const :tag "Font Size" :font-size)
|
||
|
|
(const :tag "Font Weight" :font-weight))
|
||
|
|
:value-type (choice (const :tag "None" nil)
|
||
|
|
(number)
|
||
|
|
(string)
|
||
|
|
(boolean)
|
||
|
|
(symbol)))
|
||
|
|
:group 'svg-lib)
|
||
|
|
|
||
|
|
|
||
|
|
;; Convert Emacs color to SVG color
|
||
|
|
(defun svg-lib-convert-color (color-name)
|
||
|
|
"Convert Emacs COLOR-NAME to #rrggbb form.
|
||
|
|
If COLOR-NAME is unknown to Emacs, then return COLOR-NAME as-is."
|
||
|
|
(when color-name
|
||
|
|
(let ((rgb-color (color-name-to-rgb color-name)))
|
||
|
|
(if rgb-color
|
||
|
|
(apply #'color-rgb-to-hex (append rgb-color '(2)))
|
||
|
|
color-name))))
|
||
|
|
|
||
|
|
;; SVG Library style build from partial specification
|
||
|
|
(defun svg-lib-style (&optional base &rest args)
|
||
|
|
"Build a news style using BASE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(base (or base default))
|
||
|
|
(keys (cl-loop for (key _value) on default by 'cddr
|
||
|
|
collect key))
|
||
|
|
(style '()))
|
||
|
|
|
||
|
|
(dolist (key keys)
|
||
|
|
(setq style (if (plist-member args key)
|
||
|
|
(plist-put style key (plist-get args key))
|
||
|
|
(plist-put style key (plist-get base key)))))
|
||
|
|
|
||
|
|
;; Convert emacs colors to SVG colors
|
||
|
|
(plist-put style :foreground
|
||
|
|
(svg-lib-convert-color (plist-get style :foreground)))
|
||
|
|
(plist-put style :background
|
||
|
|
(svg-lib-convert-color (plist-get style :background)))
|
||
|
|
|
||
|
|
;; Convert emacs font weights to SVG font weights
|
||
|
|
(let ((weights
|
||
|
|
'((thin . 100) (ultralight . 200) (light . 300)
|
||
|
|
(regular . 400) (medium . 500) (semibold . 600)
|
||
|
|
(bold . 700) (extrabold . 800) (black . 900))))
|
||
|
|
(plist-put style :font-weight
|
||
|
|
(or (cdr (assoc (plist-get style :font-weight) weights))
|
||
|
|
(plist-get style :font-weight))))
|
||
|
|
style))
|
||
|
|
|
||
|
|
|
||
|
|
;; Create an image displaying LABEL in a rounded box.
|
||
|
|
(defun svg-lib-tag (label &optional style &rest args)
|
||
|
|
"Create an image displaying LABEL in a rounded box using given STYLE
|
||
|
|
and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
|
||
|
|
(crop-left (plist-get style :crop-left))
|
||
|
|
(crop-right (plist-get style :crop-right))
|
||
|
|
|
||
|
|
(alignment (plist-get style :alignment))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
;; (width (plist-get style :width))
|
||
|
|
(height (plist-get style :height))
|
||
|
|
(radius (plist-get style :radius))
|
||
|
|
;; (scale (plist-get style :scale))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
(padding (plist-get style :padding))
|
||
|
|
(font-size (plist-get style :font-size))
|
||
|
|
(font-family (plist-get style :font-family))
|
||
|
|
(font-weight (plist-get style :font-weight))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
(txt-char-height (if line-spacing
|
||
|
|
(+ txt-char-height line-spacing)
|
||
|
|
txt-char-height))
|
||
|
|
(font-info (font-info (format "%s-%d" font-family font-size)))
|
||
|
|
(font-size (aref font-info 2)) ;; redefine font-size
|
||
|
|
(ascent (aref font-info 8))
|
||
|
|
(tag-char-width (aref font-info 11))
|
||
|
|
;; (tag-char-height (aref font-info 3))
|
||
|
|
(tag-width (* (+ (length label) padding) txt-char-width))
|
||
|
|
(tag-height (* txt-char-height height))
|
||
|
|
|
||
|
|
(svg-width (+ tag-width (* margin txt-char-width)))
|
||
|
|
(svg-height tag-height)
|
||
|
|
(svg-ascent (plist-get style :ascent))
|
||
|
|
|
||
|
|
(tag-x (* (- svg-width tag-width) alignment))
|
||
|
|
(text-x (+ tag-x (/ (- tag-width (* (length label) tag-char-width)) 2)))
|
||
|
|
(text-y ascent)
|
||
|
|
|
||
|
|
(tag-x (if crop-left (- tag-x txt-char-width) tag-x))
|
||
|
|
(tag-width (if crop-left (+ tag-width txt-char-width) tag-width))
|
||
|
|
(text-x (if crop-left (- text-x (/ stroke 2)) text-x))
|
||
|
|
(tag-width (if crop-right (+ tag-width txt-char-width) tag-width))
|
||
|
|
(text-x (if crop-right (+ text-x (/ stroke 2)) text-x))
|
||
|
|
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-rectangle svg tag-x 0 tag-width tag-height
|
||
|
|
:fill foreground :rx radius))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0)) (/ stroke 2.0)
|
||
|
|
(- tag-width stroke) (- tag-height stroke)
|
||
|
|
:fill background :rx (- radius (/ stroke 2.0)))
|
||
|
|
(svg-text svg label
|
||
|
|
:font-family font-family :font-weight font-weight :font-size font-size
|
||
|
|
:fill foreground :x text-x :y text-y)
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
|
||
|
|
;; Create a progress pie
|
||
|
|
(defun svg-lib-progress-pie (value &optional style &rest args)
|
||
|
|
"Create a progress pie image with value VALUE using given STYLE
|
||
|
|
and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
;; (width (plist-get style :width))
|
||
|
|
(height (plist-get style :height))
|
||
|
|
;; (scale (plist-get style :scale))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
(padding (plist-get style :padding))
|
||
|
|
;; (font-size (plist-get style :font-size))
|
||
|
|
;; (font-family (plist-get style :font-family))
|
||
|
|
;; (font-weight (plist-get style :font-weight))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
|
||
|
|
;; (font-info (font-info (format "%s-%d" font-family font-size)))
|
||
|
|
;; (ascent (aref font-info 8))
|
||
|
|
;; (tag-char-width (aref font-info 11))
|
||
|
|
;; (tag-char-height (aref font-info 3))
|
||
|
|
|
||
|
|
(tag-width (* 2 txt-char-width))
|
||
|
|
(tag-height (* txt-char-height height))
|
||
|
|
|
||
|
|
(svg-width (+ tag-width (* margin txt-char-width)))
|
||
|
|
(svg-height tag-height)
|
||
|
|
(svg-ascent (plist-get style :ascent))
|
||
|
|
|
||
|
|
;; (tag-x (/ (- svg-width tag-width) 2))
|
||
|
|
|
||
|
|
(cx (/ svg-width 2))
|
||
|
|
(cy (/ svg-height 2))
|
||
|
|
(radius (- (/ tag-height 2) (/ stroke 2)))
|
||
|
|
|
||
|
|
(iradius (- radius stroke (/ padding 2)))
|
||
|
|
|
||
|
|
(angle0 (- (/ float-pi 2)))
|
||
|
|
(x0 (+ cx (* iradius (cos angle0))))
|
||
|
|
(y0 (+ cy (* iradius (sin angle0))))
|
||
|
|
|
||
|
|
(angle1 (+ angle0 (* value 2 float-pi)))
|
||
|
|
(x1 (+ cx (* iradius (cos angle1))))
|
||
|
|
(y1 (+ cy (* iradius (sin angle1))))
|
||
|
|
|
||
|
|
(large-arc (>= (- angle1 angle0) float-pi))
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-circle svg cx cy radius :fill foreground))
|
||
|
|
|
||
|
|
(svg-circle svg cx cy (- radius (/ stroke 2.0)) :fill background)
|
||
|
|
|
||
|
|
(if (>= (- angle1 angle0) (* float-pi 2))
|
||
|
|
(svg-circle svg cx cy iradius :fill foreground)
|
||
|
|
(svg-path svg `((moveto ((,cx . ,cy)))
|
||
|
|
(lineto ((,x0 . ,y0)))
|
||
|
|
(elliptical-arc ((,iradius ,iradius ,x1 ,y1
|
||
|
|
:sweep t :large-arc ,large-arc))))
|
||
|
|
:fill foreground))
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
;; Create a progress bar
|
||
|
|
(defun svg-lib-progress-bar (value &optional style &rest args)
|
||
|
|
"Create a progress bar image with value VALUE using given STYLE
|
||
|
|
and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
(width (plist-get style :width))
|
||
|
|
(height (plist-get style :height))
|
||
|
|
(radius (plist-get style :radius))
|
||
|
|
;; (scale (plist-get style :scale))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
(padding (plist-get style :padding))
|
||
|
|
;; (font-size (plist-get style :font-size))
|
||
|
|
;; (font-family (plist-get style :font-family))
|
||
|
|
;; (font-weight (plist-get style :font-weight))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
|
||
|
|
;; (font-info (font-info (format "%s-%d" font-family font-size)))
|
||
|
|
;; (ascent (aref font-info 8))
|
||
|
|
;; (tag-char-width (aref font-info 11))
|
||
|
|
;; (tag-char-height (aref font-info 3))
|
||
|
|
|
||
|
|
(tag-width (* width txt-char-width))
|
||
|
|
(tag-height (* txt-char-height height))
|
||
|
|
|
||
|
|
(svg-width (+ tag-width (* margin txt-char-width)))
|
||
|
|
(svg-height tag-height)
|
||
|
|
(svg-ascent (plist-get style :ascent))
|
||
|
|
|
||
|
|
(tag-x (/ (- svg-width tag-width) 2))
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-rectangle svg tag-x 0 tag-width tag-height
|
||
|
|
:fill foreground :rx radius))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0))
|
||
|
|
(/ stroke 2.0)
|
||
|
|
(- tag-width stroke)
|
||
|
|
(- tag-height stroke)
|
||
|
|
:fill background :rx (- radius (/ stroke 2.0)))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0) padding)
|
||
|
|
(+ (/ stroke 2.0) padding)
|
||
|
|
(- (* value tag-width) stroke (* 2 padding))
|
||
|
|
(- tag-height stroke (* 2 padding))
|
||
|
|
:fill foreground :rx (- radius (/ stroke 2.0)))
|
||
|
|
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
;; Create a rounded box icon
|
||
|
|
(defun svg-lib--icon-get-data (collection name &optional force-reload)
|
||
|
|
"Retrieve icon NAME from COLLECTION.
|
||
|
|
|
||
|
|
Cached version is returned if it exists unless FORCE-RELOAD is t."
|
||
|
|
|
||
|
|
;; Build url from collection and name without checking for error
|
||
|
|
(let ((url (format (cdr (assoc collection svg-lib-icon-collections)) name)))
|
||
|
|
;; create the svg-lib-icons-dir if not exists
|
||
|
|
(unless (file-exists-p svg-lib-icons-dir)
|
||
|
|
(make-directory svg-lib-icons-dir t))
|
||
|
|
(let* ((filename (expand-file-name (format "%s_%s.svg" collection name) svg-lib-icons-dir))
|
||
|
|
(buffer (if (or force-reload (not (file-exists-p filename)))
|
||
|
|
(with-current-buffer (url-retrieve-synchronously url)
|
||
|
|
(goto-char (point-min))
|
||
|
|
(search-forward "\n\n")
|
||
|
|
(write-region (point) (point-max) filename)
|
||
|
|
(current-buffer))
|
||
|
|
(with-current-buffer (generate-new-buffer " *temp*")
|
||
|
|
(insert-file-contents filename)
|
||
|
|
(current-buffer)))))
|
||
|
|
(with-current-buffer buffer
|
||
|
|
(xml-parse-region (point-min) (point-max))))))
|
||
|
|
|
||
|
|
|
||
|
|
(defun svg-lib-icon (icon &optional style &rest args)
|
||
|
|
"Create a SVG image displaying icon NAME from COLLECTION using
|
||
|
|
given STYLE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(collection (plist-get style :collection))
|
||
|
|
(root (svg-lib--icon-get-data collection icon))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
(height (plist-get style :height))
|
||
|
|
(radius (plist-get style :radius))
|
||
|
|
(scale (plist-get style :scale))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
(padding (plist-get style :padding))
|
||
|
|
;; (font-size (plist-get style :font-size))
|
||
|
|
;; (font-family (plist-get style :font-family))
|
||
|
|
;; (font-weight (plist-get style :font-weight))
|
||
|
|
(width (+ 2 padding))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
(box-width (* width txt-char-width))
|
||
|
|
(box-height (* height txt-char-height))
|
||
|
|
(svg-width (+ box-width (* margin txt-char-width)))
|
||
|
|
(svg-height box-height)
|
||
|
|
(svg-ascent (plist-get style :ascent))
|
||
|
|
(box-x (/ (- svg-width box-width) 2))
|
||
|
|
(box-y 0)
|
||
|
|
|
||
|
|
;; Read original viewbox
|
||
|
|
(viewbox (cdr (assq 'viewBox (xml-node-attributes (car root)))))
|
||
|
|
(viewbox (mapcar #'string-to-number (split-string viewbox)))
|
||
|
|
(icon-x (nth 0 viewbox))
|
||
|
|
(icon-y (nth 1 viewbox))
|
||
|
|
(icon-width (nth 2 viewbox))
|
||
|
|
(icon-height (nth 3 viewbox))
|
||
|
|
(scale (* scale (/ (float box-height) (float icon-height))))
|
||
|
|
(icon-transform
|
||
|
|
(format "translate(%f,%f) scale(%f) translate(%f,%f)"
|
||
|
|
(- icon-x )
|
||
|
|
(- icon-y )
|
||
|
|
scale
|
||
|
|
(- (/ svg-width 2 scale) (/ icon-width 2))
|
||
|
|
(- (/ svg-height 2 scale) (/ icon-height 2))))
|
||
|
|
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-rectangle svg box-x box-y box-width box-height
|
||
|
|
:fill foreground :rx radius))
|
||
|
|
(when background
|
||
|
|
(svg-rectangle svg (+ box-x (/ stroke 2.0))
|
||
|
|
(+ box-y (/ stroke 2.0))
|
||
|
|
(- box-width stroke)
|
||
|
|
(- box-height stroke)
|
||
|
|
:fill background :rx (- radius (/ stroke 2.0))))
|
||
|
|
|
||
|
|
(dolist (item (xml-get-children (car root) 'path))
|
||
|
|
(let* ((attrs (xml-node-attributes item))
|
||
|
|
(path (cdr (assoc 'd attrs)))
|
||
|
|
;; (fill (or (cdr (assoc 'fill attrs)) foreground))
|
||
|
|
)
|
||
|
|
(svg-node svg 'path :d path
|
||
|
|
:fill foreground
|
||
|
|
:transform icon-transform)))
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
;; Create an image displaying LABEL in a rounded box.
|
||
|
|
(defun svg-lib-icon+tag (icon label &optional style &rest args)
|
||
|
|
"Create an image displaying LABEL in a rounded box using given STYLE
|
||
|
|
and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(collection (plist-get style :collection))
|
||
|
|
(root (svg-lib--icon-get-data collection icon))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
;; (width (plist-get style :width))
|
||
|
|
(height (plist-get style :height))
|
||
|
|
(radius (plist-get style :radius))
|
||
|
|
(scale (plist-get style :scale))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
(padding (plist-get style :padding))
|
||
|
|
(font-size (plist-get style :font-size))
|
||
|
|
(font-family (plist-get style :font-family))
|
||
|
|
(font-weight (plist-get style :font-weight))
|
||
|
|
|
||
|
|
(label-length (+ (length label) 2))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
;; (box-width (* width txt-char-width))
|
||
|
|
;; (box-height (* height txt-char-height))
|
||
|
|
|
||
|
|
(font-info (font-info (format "%s-%d" font-family font-size)))
|
||
|
|
(ascent (aref font-info 8))
|
||
|
|
(tag-char-width (aref font-info 11))
|
||
|
|
;; (tag-char-height (aref font-info 3))
|
||
|
|
(tag-width (* (+ label-length padding) txt-char-width))
|
||
|
|
(tag-height (* txt-char-height height))
|
||
|
|
|
||
|
|
(svg-width (+ tag-width (* margin txt-char-width)))
|
||
|
|
(svg-height tag-height)
|
||
|
|
(svg-ascent (plist-get style :ascent))
|
||
|
|
|
||
|
|
(tag-x (/ (- svg-width tag-width) 2))
|
||
|
|
(text-x (+ tag-x (/ (- tag-width (* (length label) tag-char-width)) 2)))
|
||
|
|
(text-x (+ text-x tag-char-width))
|
||
|
|
(text-y ascent)
|
||
|
|
|
||
|
|
;; ;; Read original viewbox
|
||
|
|
(viewbox (cdr (assq 'viewBox (xml-node-attributes (car root)))))
|
||
|
|
(viewbox (mapcar 'string-to-number (split-string viewbox)))
|
||
|
|
(icon-x (nth 0 viewbox))
|
||
|
|
(icon-y (nth 1 viewbox))
|
||
|
|
(icon-width (nth 2 viewbox))
|
||
|
|
(icon-height (nth 3 viewbox))
|
||
|
|
(scale (* scale (/ (float tag-height) (float icon-height))))
|
||
|
|
(icon-transform
|
||
|
|
(format "translate(%f,%f) scale(%f) translate(%f,%f)"
|
||
|
|
(- icon-x )
|
||
|
|
(- icon-y )
|
||
|
|
scale
|
||
|
|
(- (/ (- text-x (* tag-char-width 1.25)) scale) (/ icon-width 2))
|
||
|
|
(- (/ svg-height 2 scale) (/ icon-height 2))))
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-rectangle svg tag-x 0 tag-width tag-height
|
||
|
|
:fill foreground :rx radius))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0)) (/ stroke 2.0)
|
||
|
|
(- tag-width stroke) (- tag-height stroke)
|
||
|
|
:fill background :rx (- radius (/ stroke 2.0)))
|
||
|
|
(svg-text svg label
|
||
|
|
:font-family font-family :font-weight font-weight :font-size font-size
|
||
|
|
:fill foreground :x text-x :y text-y)
|
||
|
|
|
||
|
|
|
||
|
|
(dolist (item (xml-get-children (car root) 'path))
|
||
|
|
(let* ((attrs (xml-node-attributes item))
|
||
|
|
(path (cdr (assoc 'd attrs)))
|
||
|
|
;; (fill (or (cdr (assoc 'fill attrs)) foreground))
|
||
|
|
)
|
||
|
|
(svg-node svg 'path :d path
|
||
|
|
:fill foreground
|
||
|
|
:transform icon-transform)))
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
(defun svg-lib-date (&optional date style &rest args)
|
||
|
|
"Create a two lines date icon showing given DATE, using given
|
||
|
|
STYLE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((date (or date (current-time)))
|
||
|
|
(month (upcase (format-time-string "%b" date)))
|
||
|
|
(day (format-time-string "%d" date)))
|
||
|
|
(apply 'svg-lib-box month day style args)))
|
||
|
|
|
||
|
|
(defun svg-lib-week-date (&optional date style &rest args)
|
||
|
|
"Create a two lines date icon showing given DATE, using given
|
||
|
|
STYLE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((date (or date (current-time)))
|
||
|
|
(week (format-time-string "%W" date)))
|
||
|
|
(apply 'svg-lib-box "WEEK" week style args)))
|
||
|
|
|
||
|
|
(defun svg-lib-day-date (&optional date style &rest args)
|
||
|
|
"Create a two lines date icon showing given DATE, using given
|
||
|
|
STYLE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((weekday (upcase (format-time-string "%a" date)))
|
||
|
|
(day (format-time-string "%d" date)))
|
||
|
|
(apply 'svg-lib-box weekday day style args)))
|
||
|
|
|
||
|
|
|
||
|
|
(defun svg-lib-box (top bottom &optional style &rest args)
|
||
|
|
"Create a two lines icon showing given TOP and BOTTOM text, using
|
||
|
|
given STYLE and style elements ARGS."
|
||
|
|
|
||
|
|
(let* ((default svg-lib-style-default)
|
||
|
|
(style (if style (apply #'svg-lib-style nil style) default))
|
||
|
|
(style (if args (apply #'svg-lib-style style args) style))
|
||
|
|
|
||
|
|
(foreground (plist-get style :foreground))
|
||
|
|
(background (plist-get style :background))
|
||
|
|
(alignment (plist-get style :alignment))
|
||
|
|
(stroke (plist-get style :stroke))
|
||
|
|
(width (or (plist-get args :width) 5))
|
||
|
|
(height (or (plist-get args :height) 2))
|
||
|
|
(radius (plist-get style :radius))
|
||
|
|
(margin (plist-get style :margin))
|
||
|
|
|
||
|
|
(font-size (plist-get style :font-size))
|
||
|
|
(font-family (plist-get style :font-family))
|
||
|
|
(font-weight (plist-get style :font-weight))
|
||
|
|
|
||
|
|
(txt-char-width (window-font-width))
|
||
|
|
(txt-char-height (window-font-height))
|
||
|
|
|
||
|
|
(font-info (font-info (format "%s-%d" font-family font-size)))
|
||
|
|
(ascent (aref font-info 8))
|
||
|
|
(tag-char-width (aref font-info 11))
|
||
|
|
(tag-char-height (aref font-info 3))
|
||
|
|
(tag-width (* width txt-char-width))
|
||
|
|
|
||
|
|
(tag-height (* height txt-char-height))
|
||
|
|
(svg-width (+ tag-width (* margin txt-char-width)))
|
||
|
|
(svg-height tag-height)
|
||
|
|
(svg-ascent (or (plist-get style :ascent) 'center))
|
||
|
|
(tag-x (/ (- svg-width tag-width) 2) )
|
||
|
|
|
||
|
|
(svg (svg-create svg-width svg-height)))
|
||
|
|
|
||
|
|
(when (>= stroke 0.25)
|
||
|
|
(svg-rectangle svg tag-x 0 tag-width tag-height
|
||
|
|
:fill foreground :rx radius))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0))
|
||
|
|
(/ stroke 2.0)
|
||
|
|
(- tag-width stroke)
|
||
|
|
(- tag-height stroke)
|
||
|
|
:fill background :rx (- radius (/ stroke 2.0)))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0))
|
||
|
|
(/ stroke 2.0)
|
||
|
|
(- tag-width stroke)
|
||
|
|
(- (/ tag-height 2) stroke)
|
||
|
|
:fill foreground :rx (- radius (/ stroke 2.0)))
|
||
|
|
(svg-rectangle svg (+ tag-x (/ stroke 2.0))
|
||
|
|
(+ (/ stroke 2.0) (/ tag-height 3))
|
||
|
|
(- tag-width stroke)
|
||
|
|
(- (/ tag-height 2) stroke)
|
||
|
|
:fill background :rx 0)
|
||
|
|
(svg-text svg top
|
||
|
|
:font-family font-family
|
||
|
|
:font-weight "bold"
|
||
|
|
:font-size (* font-size 0.9)
|
||
|
|
:fill background
|
||
|
|
:text-anchor "middle"
|
||
|
|
:x (/ svg-width 2)
|
||
|
|
:y "+0.95em")
|
||
|
|
(svg-text svg bottom
|
||
|
|
:font-family font-family
|
||
|
|
:font-weight "bold"
|
||
|
|
:font-size (* font-size 1.7)
|
||
|
|
:fill foreground ;;(face-foreground 'default)
|
||
|
|
:text-anchor "middle"
|
||
|
|
:x (/ svg-width 2)
|
||
|
|
:y "+1.6em")
|
||
|
|
(svg-lib--image svg :ascent svg-ascent)))
|
||
|
|
|
||
|
|
(defun svg-lib-concat (svg-image-1 svg-image-2)
|
||
|
|
"Concatenate two svg images horizontally."
|
||
|
|
|
||
|
|
(let* ((svg (car (with-temp-buffer
|
||
|
|
(insert (plist-get (cdr svg-image-1) :data))
|
||
|
|
(xml-parse-region (point-min) (point-max)))))
|
||
|
|
(attrs (xml-node-attributes svg))
|
||
|
|
(width-1 (string-to-number (cdr (assq 'width attrs))))
|
||
|
|
(height-1 (string-to-number (cdr (assq 'height attrs))))
|
||
|
|
(children-1 (xml-node-children svg))
|
||
|
|
|
||
|
|
(svg (car (with-temp-buffer
|
||
|
|
(insert (plist-get (cdr svg-image-2) :data))
|
||
|
|
(xml-parse-region (point-min) (point-max)))))
|
||
|
|
(attrs (xml-node-attributes svg))
|
||
|
|
(width-2 (string-to-number (cdr (assq 'width attrs))))
|
||
|
|
(height-2 (string-to-number (cdr (assq 'height attrs))))
|
||
|
|
(children-2 (xml-node-children svg))
|
||
|
|
|
||
|
|
(width (+ width-1 width-2))
|
||
|
|
(height (max height-1 height-2))
|
||
|
|
(transform (format "translate(%f,0)" width-1))
|
||
|
|
(svg (svg-create width height)))
|
||
|
|
|
||
|
|
(dolist (child children-1)
|
||
|
|
(dom-append-child svg child))
|
||
|
|
|
||
|
|
(dolist (child children-2)
|
||
|
|
(unless (stringp child)
|
||
|
|
(dom-set-attribute child 'transform transform))
|
||
|
|
(dom-append-child svg child))
|
||
|
|
svg))
|
||
|
|
|
||
|
|
|
||
|
|
(defvar svg-lib-button--id-counter 0
|
||
|
|
"SVG button unique id counter")
|
||
|
|
|
||
|
|
(defun svg-lib-button--search (id)
|
||
|
|
"Return region for the button with given ID"
|
||
|
|
|
||
|
|
(save-excursion
|
||
|
|
(goto-char (point-min))
|
||
|
|
(save-match-data
|
||
|
|
(when-let* ((match (text-property-search-forward 'button-id id t)))
|
||
|
|
(cons (prop-match-beginning match)
|
||
|
|
(prop-match-end match))))))
|
||
|
|
|
||
|
|
(defun svg-lib-button--at-point (&optional pos)
|
||
|
|
"Return the button at point"
|
||
|
|
(get-text-property (or pos (point)) 'button-id))
|
||
|
|
|
||
|
|
(defun svg-lib-button--get-state (id &optional region)
|
||
|
|
"Return the state of button ID"
|
||
|
|
|
||
|
|
(when-let* ((region (or region (svg-lib-button--search id))))
|
||
|
|
(get-text-property (car region) 'button-state)))
|
||
|
|
|
||
|
|
(defun svg-lib-button--set-state (id state &optional no-reset)
|
||
|
|
"Set the state of button ID to STATE, reset the state of any
|
||
|
|
hovered button unless NO-RESET is t"
|
||
|
|
|
||
|
|
;; Reset previous hover button state (if any)
|
||
|
|
(when (and (boundp 'svg-lib-button--hover-id) svg-lib-button--hover-id (not no-reset))
|
||
|
|
(let ((prev-id svg-lib-button--hover-id))
|
||
|
|
(setq-local svg-lib-button--hover-id nil)
|
||
|
|
(svg-lib-button--set-state prev-id 'active)))
|
||
|
|
|
||
|
|
;; Set new state
|
||
|
|
(when-let* ((region (svg-lib-button--search id))
|
||
|
|
(cur-state (svg-lib-button--get-state id region))
|
||
|
|
(button-list (get-text-property (car region) 'button-list))
|
||
|
|
(display (cdr (assoc state button-list))))
|
||
|
|
(put-text-property (car region) (cdr region) 'display display)
|
||
|
|
(put-text-property (car region) (cdr region) 'button-state state)
|
||
|
|
(cond ((eq state 'hover)
|
||
|
|
(setq-local svg-lib-button--hover-id id))
|
||
|
|
((eq state 'press)
|
||
|
|
(setq-local svg-lib-button--press-id id)))))
|
||
|
|
|
||
|
|
(defun svg-lib-button--tooltip-hide (&rest args)
|
||
|
|
"Set currently press or hightlighted button to default
|
||
|
|
state (active) and hover button at point if any."
|
||
|
|
|
||
|
|
(when (boundp 'svg-lib-button--press-id)
|
||
|
|
(svg-lib-button--set-state svg-lib-button--press-id 'active))
|
||
|
|
(when (boundp 'svg-lib-button--hover-id)
|
||
|
|
(svg-lib-button--set-state svg-lib-button--hover-id 'active))
|
||
|
|
|
||
|
|
;; Hover button at point (if any)
|
||
|
|
(svg-lib-button--set-state (svg-lib-button--at-point) 'hover)
|
||
|
|
(advice-remove 'tooltip-hide #'svg-lib-button--tooltip-hide))
|
||
|
|
|
||
|
|
(defun svg-lib-button--tooltip-show (pos)
|
||
|
|
"Set button under mouse state to hover or press depending
|
||
|
|
on whether mouse button 1 is down (press) or up (hover)"
|
||
|
|
|
||
|
|
(if (and (consp last-input-event)
|
||
|
|
(string-match-p "down-mouse-1" (format "%s" (car last-input-event))))
|
||
|
|
(svg-lib-button--set-state (svg-lib-button--at-point pos) 'press)
|
||
|
|
(svg-lib-button--set-state (svg-lib-button--at-point pos) 'hover))
|
||
|
|
(advice-add 'tooltip-hide :before #'svg-lib-button--tooltip-hide))
|
||
|
|
|
||
|
|
(defun svg-lib-button--mouse-down ()
|
||
|
|
"Set button under mouse state to press."
|
||
|
|
|
||
|
|
(interactive)
|
||
|
|
(save-excursion
|
||
|
|
(mouse-set-point last-input-event)
|
||
|
|
(svg-lib-button--set-state (svg-lib-button--at-point) 'press))
|
||
|
|
(advice-add 'tooltip-hide :before #'svg-lib-button--tooltip-hide))
|
||
|
|
|
||
|
|
(defun svg-lib-button--mouse-press ()
|
||
|
|
"Set button under mouse state to default state (active) and call
|
||
|
|
button hook. If current buffer is minibuffer, it aborts it. It
|
||
|
|
would be better to simply exit minibuffer but this leads to focus
|
||
|
|
problem if the hook creates a frame."
|
||
|
|
|
||
|
|
(interactive)
|
||
|
|
|
||
|
|
;; Here we check if mouse is still over the button
|
||
|
|
(let ((mouse-point (save-excursion
|
||
|
|
(mouse-set-point last-input-event)
|
||
|
|
(point))))
|
||
|
|
(if-let ((id (svg-lib-button--at-point mouse-point)))
|
||
|
|
(svg-lib-button--set-state svg-lib-button--press-id 'hover)
|
||
|
|
(svg-lib-button--set-state svg-lib-button--press-id 'active)))
|
||
|
|
|
||
|
|
(when-let* ((region (svg-lib-button--search svg-lib-button--press-id))
|
||
|
|
(hook (get-text-property (car region) 'button-hook)))
|
||
|
|
(if (minibufferp nil t)
|
||
|
|
(unwind-protect
|
||
|
|
(abort-minibuffers)
|
||
|
|
(funcall hook))
|
||
|
|
(funcall hook))))
|
||
|
|
|
||
|
|
(defun svg-lib-button--mouse-drag ()
|
||
|
|
"Update the state of the button under mouse"
|
||
|
|
|
||
|
|
(interactive)
|
||
|
|
(save-excursion
|
||
|
|
(mouse-set-point last-input-event)
|
||
|
|
(svg-lib-button--set-state (svg-lib-button--at-point) 'press)))
|
||
|
|
|
||
|
|
(defun svg-lib-button--make (label &optional face)
|
||
|
|
"Return a svg tag with given LABEL and FACE. LABEL can be composed
|
||
|
|
as \"[collection:icon] label\" resulting in an icon+tag button."
|
||
|
|
|
||
|
|
(save-match-data
|
||
|
|
(let* ((face (or face 'default))
|
||
|
|
(label-regex "\\[\\([a-zA-Z0-9]+:\\)?\\([a-zA-Z0-9 _-]+\\)\\] *\\(.+\\)"))
|
||
|
|
(if (string-match label-regex label)
|
||
|
|
(let* ((collection (match-string 1 label))
|
||
|
|
(collection (if (stringp collection)
|
||
|
|
(substring collection 0 -1)
|
||
|
|
(plist-get svg-lib-style-default ':collection)))
|
||
|
|
(icon (match-string 2 label))
|
||
|
|
(label (match-string 3 label)))
|
||
|
|
(svg-lib-icon+tag icon label nil
|
||
|
|
:collection collection
|
||
|
|
:stroke (or (plist-get (face-attribute face :box) ':line-width) 0)
|
||
|
|
:font-family (face-attribute face :family nil t)
|
||
|
|
:font-weight (face-attribute face :weight nil t)
|
||
|
|
:foreground (face-foreground face nil 'default)
|
||
|
|
:background (face-background face nil 'default)))
|
||
|
|
(svg-lib-tag label nil
|
||
|
|
:stroke (or (plist-get (face-attribute face :box) ':line-width) 0)
|
||
|
|
:font-family (face-attribute face :family nil t)
|
||
|
|
:font-weight (face-attribute face :weight nil t)
|
||
|
|
:foreground (face-foreground face nil 'default)
|
||
|
|
:background (face-background face nil 'default))))))
|
||
|
|
|
||
|
|
|
||
|
|
(defun svg-lib-button (label &optional hook help active-face hover-face press-face)
|
||
|
|
"Make a button with given LABEL that will call HOOK when
|
||
|
|
pressed. The HELP text is displatyed when mouse is hovering the
|
||
|
|
button. ACTIVE-FACE, HOVER-FACE and PRESS-FACE correspond to the
|
||
|
|
different states of the button. LABEL can be composed as
|
||
|
|
\"[collection:icon] label\" resulting in an icon+tag button.
|
||
|
|
|
||
|
|
For proper highlighting, `svg-lib-button-mode' needs to be
|
||
|
|
activated before inserting a button into a buffer."
|
||
|
|
|
||
|
|
;; Having a SVG button highlighted when mouse cursor is hovering is
|
||
|
|
;; not totally straightforward because Emacs lacks the proper
|
||
|
|
;; machinery to do so. There is actually a mouse-face property but it
|
||
|
|
;; only changes the face and cannot change the display property (that
|
||
|
|
;; is needed for SVG). To solve the problem, we can take advantage of
|
||
|
|
;; the tooltip machinery because it offers a hackable enter/exit event
|
||
|
|
;; that can be used to update the display at the proper time.
|
||
|
|
;;
|
||
|
|
;; To make this works, a few properties needs to be removed from
|
||
|
|
;; `font-lock-extra-managed-props' and `yank-excluded-properties'.
|
||
|
|
;; For org-mode, another hack is necessary because when org-mode
|
||
|
|
;; unfontifies a region (see `org-unfontify-region'), it removes the
|
||
|
|
;; local keymap that is used. You thus need to activate the
|
||
|
|
;; svg-lib-button-mode to have this set for you.
|
||
|
|
|
||
|
|
(let* ((active (svg-lib-button--make label
|
||
|
|
(or active-face 'svg-lib-button-active-face)))
|
||
|
|
(hover (svg-lib-button--make label
|
||
|
|
(or hover-face 'svg-lib-button-hover-face)))
|
||
|
|
(press (svg-lib-button--make label
|
||
|
|
(or press-face 'svg-lib-button-press-face)))
|
||
|
|
(buttons `((active . ,active)
|
||
|
|
(hover . ,hover)
|
||
|
|
(press . ,press)))
|
||
|
|
(state 'active))
|
||
|
|
(setq svg-lib-button--id-counter (1+ svg-lib-button--id-counter))
|
||
|
|
(propertize (concat label " ")
|
||
|
|
'display (cdr (assoc state buttons))
|
||
|
|
'svg-lib-button t
|
||
|
|
'button-id svg-lib-button--id-counter
|
||
|
|
'button-state state
|
||
|
|
'button-list buttons
|
||
|
|
'button-hook hook
|
||
|
|
'front-sticky nil
|
||
|
|
'rear-nonsticky t
|
||
|
|
'keymap (define-keymap :suppress t
|
||
|
|
"<down-mouse-1>" #'svg-lib-button--mouse-down
|
||
|
|
"<mouse-1>" #'svg-lib-button--mouse-press
|
||
|
|
"<drag-mouse-1>" #'svg-lib-button--mouse-drag)
|
||
|
|
'help-echo `(lambda (_window _object pos)
|
||
|
|
(svg-lib-button--tooltip-show pos)
|
||
|
|
,help)
|
||
|
|
'pointer 'hand)))
|
||
|
|
|
||
|
|
(defun svg-lib-button--remove-text-properties (orig-fun beg end properties &optional object)
|
||
|
|
"This advice function ensures keymap is not removed when in svg-lib-button-mode"
|
||
|
|
|
||
|
|
(let ((properties (if (and svg-lib-button-mode (derived-mode-p 'org-mode))
|
||
|
|
(org-plist-delete properties 'keymap)
|
||
|
|
properties)))
|
||
|
|
(apply orig-fun (list beg end properties object))))
|
||
|
|
|
||
|
|
(define-minor-mode svg-lib-button-mode
|
||
|
|
"Activate svg-lib-button-mode that takes care of activating tooltip
|
||
|
|
mode and removing some properties from `yank-excluded-properties'
|
||
|
|
and `font-lock-extra-managed-props' in order for highlight to
|
||
|
|
work properly. This mode also installs an advice on
|
||
|
|
`remove-text-properties' in org-mode in order to not delete the
|
||
|
|
`keymap' property that is necessary to detect mouse press events."
|
||
|
|
|
||
|
|
:lighter "B"
|
||
|
|
|
||
|
|
(when svg-lib-button-mode
|
||
|
|
;; This is necessary for detecting when mouse cursor enter or
|
||
|
|
;; leave a button
|
||
|
|
(require 'tooltip)
|
||
|
|
(tooltip-mode 1)
|
||
|
|
|
||
|
|
;; This is necessary for preventing org-mode to remove keymap when
|
||
|
|
;; unfontiying a region
|
||
|
|
(advice-add #'remove-text-properties :around #'svg-lib-button--remove-text-properties)
|
||
|
|
|
||
|
|
;; This prevents help-echo to be removed.when button is copied/yanked
|
||
|
|
(dolist (property '(help-echo keymap))
|
||
|
|
(setq-local yank-excluded-properties
|
||
|
|
(remove property yank-excluded-properties)))
|
||
|
|
|
||
|
|
;; This prevents help-echo to be removed.by font-lock
|
||
|
|
(dolist (property '(help-echo keymap display))
|
||
|
|
(setq-local font-lock-extra-managed-props
|
||
|
|
(remove property font-lock-extra-managed-props))))
|
||
|
|
|
||
|
|
(unless svg-lib-button-mode
|
||
|
|
(advice-remove #'remove-text-properties #'svg-lib-button--remove-text-properties)))
|
||
|
|
|
||
|
|
(provide 'svg-lib)
|
||
|
|
;;; svg-lib.el ends here
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|