Mouse Events on Images in Emacs

emacs

For a project I'm working on I wanted to draw a SVG image in Emacs and update it when the user clicks on it. After digging through the documentation for a while, I found a rather nice solution.

This page contains the code for a simple SVG editor where dragging the mouse draws a line and right-clicking draws a circle.

1.svg

Imports

Emacs provides a library for creating SVG images. We also need subr and subr-x for some helper functions.

(require 'subr)
(require 'subr-x)
(require 'svg)
2.svg

Variables

All constants a defined with variables. A more serious application should probably use defcustom for this.

There are also buffer-local variables to keep track of the lines and circles that have been drawn, and whether the grid should be shown.

(defvar mouse-demo-width 800
  "Width of the mouse-demo SVG.")
(defvar mouse-demo-height 800
  "Height of the mouse-demo SVG.")
(defvar mouse-demo-grid-size 40
  "Size of the optional grid.")

(defvar-local mouse-demo-lines nil
  "Lines in the current buffer.")
(defvar-local mouse-demo-circles nil
  "Circles in the current buffer.")
(defvar-local mouse-demo-grid nil
  "Enable/disable the grid.")
3.svg

Keymap and Mode Definition

Handlers for mouse events can be defined in the keymap of a mode. On my mouse, mouse-1 is the left button and mouse-3 is the right one.

See the Emacs Documentation for more details on supported mouse event types.

(defvar mouse-demo-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map [drag-mouse-1] #'mouse-demo-handle-drag)
    (define-key map [down-mouse-3] #'mouse-demo-handle-click)
    (define-key map (kbd "g") #'mouse-demo-toggle-grid)
    (define-key map (kbd "c") #'mouse-demo-clear)
    map)
  "Keymap for `mouse-demo-mode'.")

(define-minor-mode mouse-demo-mode
  "Minor mode for drawing lines and circles in SVG images.

\\{mouse-demo-mode-map}"
  :init-value nil
  :lighter " mouse-demo"
  :keymap mouse-demo-mode-map
  :group 'mouse-demo)

There is no key-binding for saving the image as this can be done with the image-save command.

4.svg

Positions Relative to an Image

Each time a drag- or click-event occurs, we want to find out the x-y position of the event relative to the image.

(posn-object posn) returns the object at posn . For images, this is a list (image :key1 value1 :key2 value2 ...) .

One thing to look out for is that images might be scaled when inserting them in a buffer, so we need to divide the x and y coordinates by the scale of the image.

I have also included a snap-to-grid functionality, rounding x and y to the nearest point on a grid.

(defun mouse-demo-image-xy (posn)
  "If POSN is on an image, return a position '(x . y)' relative
to the image."
  (let ((obj (posn-object posn)))
    (when (eq 'image (car obj))
      (let* ((scale (plist-get (cdr obj) :scale))
             (posn-xy (posn-object-x-y posn))
             (x (/ (car posn-xy) scale))
             (y (/ (cdr posn-xy) scale)))
        (if mouse-demo-grid
            (cons
             (* mouse-demo-grid-size (round x mouse-demo-grid-size))
             (* mouse-demo-grid-size (round y mouse-demo-grid-size)))
          (cons x y))))))
5.svg

Event Handlers

Event handlers are called interactively and with (interactive "e") they receive the event as their argument.

(event-start event) returns the position where the event started, the drag-event also has a (event-end event) .

(defun mouse-demo-toggle-grid ()
  "Toggle the grid."
  (interactive)
  (setq mouse-demo-grid (not mouse-demo-grid))
  (mouse-demo-redraw))

(defun mouse-demo-clear ()
  "Remove all lines and circles."
  (interactive)
  (setq mouse-demo-lines '()
        mouse-demo-circles '())
  (mouse-demo-redraw))

(defun mouse-demo-handle-drag (event)
  "Draw a line from the start of EVENT to its end."
  (interactive "e")
  (when-let ((start (mouse-demo-image-xy (event-start event)))
             (end (mouse-demo-image-xy (event-end event))))
    (push (cons start end) mouse-demo-lines)
    (mouse-demo-redraw)))

(defun mouse-demo-handle-click (event)
  "Draw a circle at the position of EVENT."
  (interactive "e")
  (when-let ((xy (mouse-demo-image-xy (event-start event))))
    (push xy mouse-demo-circles)
    (mouse-demo-redraw)))
6.svg

Drawing SVGs

(defun mouse-demo-svg ()
  "Generate a SVG with lines, circles and an optional grid."
  (let ((svg (svg-create mouse-demo-width mouse-demo-height)))
    ;; Background
    (svg-rectangle
     svg 2 2 (- mouse-demo-width 4) (- mouse-demo-height 4)
     :fill-color "white"
     :stroke-color "black"
     :stroke-width 4)

    ;; Grid
    (when mouse-demo-grid
      (dolist (x (number-sequence 0 mouse-demo-width mouse-demo-grid-size))
        (svg-line
         svg x 0 x mouse-demo-height
         :stroke-color "gray"
         :stroke-width 1))
      (dolist (y (number-sequence 0 mouse-demo-height mouse-demo-grid-size))
        (svg-line
         svg 0 y mouse-demo-width y
         :stroke-color "gray"
         :stroke-width 1)))

    ;; Lines
    (dolist (line mouse-demo-lines)
      (svg-line
       svg (caar line) (cdar line) (cadr line) (cddr line)
       :stroke-color "black"
       :stroke-width 4
       :stroke-linecap "round"))

    ;; Circles
    (dolist (circle mouse-demo-circles)
      (svg-circle
       svg (car circle) (cdr circle) 20
       :fill-color "red"))

    (svg-image svg)))
7.svg

Displaying SVGs

All that's left to do is to open a buffer in mouse-demo-mode and insert the SVG we've drawn.

The (insert "\n ") adds some padding at the left and the top.

(defun mouse-demo-buffer ()
  (get-buffer-create "*mouse-demo*"))

(defun mouse-demo-redraw ()
  "Redraw the buffer."
  (with-current-buffer (mouse-demo-buffer)
    (goto-char (point-min))
    (erase-buffer)
    (insert "\n ")
    (insert-image (mouse-demo-svg))))

(defun mouse-demo ()
  (interactive)
  (with-current-buffer (mouse-demo-buffer)
    (mouse-demo-mode)
    (mouse-demo-clear)
    (switch-to-buffer (current-buffer))))
8.svg

If you have an idea how this page could be improved or a comment send me a mail.