Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing input-handling functions. #128

Merged
merged 4 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,6 @@ TUTORIAL> ;; ready
#+END_SRC

** Tutorial
/NOTE: This tutorial is using the revised/ =DEFSKETCH= /macro, introduced in May 2016. Until this release hits Quicklisp, you'll have to install Sketch manually to your/ =local-projects= /directory, along with https://github.com/lispgames/cl-sdl2 and
https://github.com/lispgames/sdl2kit. More about this [[https://github.com/vydd/sketch/issues/12][here]]./

Defining sketches is done with the =DEFSKETCH= macro, that wraps =DEFCLASS=. Using =DEFCLASS= is still possible, but =DEFSKETCH= makes everything so much easier, and in these examples, we're going to pretend that's the only way.

#+BEGIN_SRC lisp
Expand Down Expand Up @@ -432,40 +429,46 @@ Note that =load-resource= automatically caches the resource when it is called in
Images can be cropped using =(crop (image-resource image) x y w h)=, where =x= and =y= indicate the top-left corner of the cropping rectangle and =w= and =h= indicate the width & height. Image flipping can be accomplished by using negative =w= and =h= values.

*** Input
Input is handled by overriding methods provided by [[https://github.com/lispgames/sdl2kit/#Windows][sdl2kit]]; the sketch window is a subclass of the =WINDOW= class provided by =sdl2kit=, which provides these generic functions for handling input. Drawing functions cannot be called from these methods, only from the body of =defsketch=.
Input is handled by defining implementations of the methods listed below. Currently, it is not possible to call drawing functions from these methods, though this can be worked around by saving the input somewhere and then doing the drawing from the sketch body, as demonstrated in the examples to follow.

- =(on-click instance x y)= is called when there's a left click. =x= and =y= give the coordinates of the click.
- =(on-middle-click instance x y)= and =(on-right-click instance x y)= for middle and right clicks.
- =(on-click-press instance x y)=, =(on-middle-click-press instance x y)= and =(on-right-click-press instance x y)= are specifically called when the corresponding mouse button is initially pressed down.
- =(on-hover instance x y)= is called when the mouse moves, =x= and =y= give its coordinates.
- =(on-text instance text)= is called when a single character is entered, =text= is a string consisting of just this character.
- =(on-key instance key state)= is called when a key is pressed. =key= is a keyword symbol denoting which key was pressed/released (like =:space= or =:left=; for now, the names are based on SDL2 scancodes, see [[https://wiki.libsdl.org/SDL2/SDL_Scancode][here]] for the full list), and =state= is a keyword symbol denoting whether the key was pressed (=:keyup=) or released (=:keydown=).

In this example, we draw a new rectangle every time there is a click. The important parameters of the event method: =state= is a keyword symbol indicating the state of the mouse, =ts= is a timestamp, =x= and =y= are the coordinates of the mouse click.
In this example, we draw a new rectangle every time there is a click.

#+BEGIN_SRC lisp
(defsketch input-test
((title "Hello, input")
(rectangles nil))
(loop for (x y) in rectangles
do (rect x y 50 50)))
(defmethod kit.sdl2:mousebutton-event ((window input-test) state ts b x y)
(defmethod on-click ((window input-test) x y)
(with-slots (rectangles) window
(when (eq state :mousebuttondown)
(push (list x y) rectangles))))
(push (list x y) rectangles)))
#+END_SRC

In this example, all keyboard text input is echoed to the screen. =text= is a string / character array containing a single character.
In this example, all keyboard text input is echoed to the screen.

#+BEGIN_SRC lisp
(defsketch input-test
(defsketch text-test
((title "Hello, input")
(text-to-write nil))
(loop for s in text-to-write
do (text s 0 0 20 20)
do (translate 20 0)))
(defmethod kit.sdl2:textinput-event ((window input-test) ts text)
(defmethod on-text ((window text-test) text)
(with-slots (text-to-write) window
(setf text-to-write (nconc text-to-write (list text)))))
#+END_SRC

Finally, here is an example where a pair of eyes follow the mouse (the pupils are restricted to a rectangle, it would look better if they were restricted to a circle).

#+BEGIN_SRC lisp
(defsketch input-test
(defsketch hover-test
((looking-at (list 0 0))
(cx (/ width 2))
(cy (/ height 2)))
Expand All @@ -484,13 +487,13 @@ Finally, here is an example where a pair of eyes follow the mouse (the pupils ar
(* (signum diff) 10))))))
(circle (move-towards cx-1 mx) (move-towards cy my) 10)
(circle (move-towards cx-2 mx) (move-towards cy my) 10)))))
(defmethod kit.sdl2:mousemotion-event ((window input-test) ts bm x y xrel yrel)
(defmethod on-hover ((window hover-test) x y)
(with-slots (looking-at) window
(setf (car looking-at) x
(cadr looking-at) y)))
#+END_SRC

See also: [[https://github.com/vydd/sketch/blob/master/examples/life.lisp][life.lisp]] and [[https://github.com/vydd/sketch/blob/master/examples/input.lisp][input.lisp]].
See also: [[https://github.com/vydd/sketch/blob/master/examples/life.lisp][life.lisp]].

*** Setup
The generic function =(setup instance &key &allow-other-keys)= is a hook that gets called once on every "restart" of the sketch. That is:
Expand Down Expand Up @@ -519,7 +522,7 @@ Here is an example usage of =setup= from [[https://github.com/vydd/sketch/blob/m
(when should-save
(setf should-save nil)
(save-png "/tmp/my-sketch.png")))
(defmethod kit.sdl2:textinput-event ((window save-test) ts text)
(defmethod on-text ((window save-test) text)
(when (string= text "s")
(setf (slot-value window 'should-save) t)))
#+END_SRC
Expand Down
19 changes: 9 additions & 10 deletions examples/life.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,14 @@
(when running
(setf front (mod (1+ front) 2)))))

(defmethod kit.sdl2:textinput-event ((window life) ts text)
(with-slots (running) window
(defmethod on-text ((instance life) text)
(with-slots (running) instance
(setf running (not running))))

(defmethod kit.sdl2:mousebutton-event ((window life) state ts b x y)
(when (eq state :mousebuttondown)
(with-slots (cells front running cell-size) window
(when (not running)
(let ((cy (1+ (truncate (/ y cell-size))))
(cx (1+ (truncate (/ x cell-size)))))
(setf (aref cells cy cx front)
(mod (1+ (aref cells cy cx front)) 2)))))))
(defmethod on-click ((instance life) x y)
(with-slots (cells front running cell-size) instance
(when (not running)
(let ((cy (1+ (truncate (/ y cell-size))))
(cx (1+ (truncate (/ x cell-size)))))
(setf (aref cells cy cx front)
(mod (1+ (aref cells cy cx front)) 2))))))
74 changes: 51 additions & 23 deletions src/controllers.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
(defmethod on-click (instance x y))
(defmethod on-middle-click (instance x y))
(defmethod on-right-click (instance x y))
(defmethod on-click-press (instance x y))
(defmethod on-middle-click-press (instance x y))
(defmethod on-right-click-press (instance x y))
(defmethod on-hover (instance x y))
(defmethod on-enter (instance))
(defmethod on-leave (instance))


(defun propagate-to-entity (sketch x y f)
(loop
for entity being the hash-key of (sketch-%entities sketch)
Expand All @@ -29,23 +31,20 @@
when (and (< 0 ix iw) (< 0 iy ih))
do (funcall f entity ix iy)))

(defmethod on-click :around ((*sketch* sketch) x y)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(propagate-to-entity *sketch* x y #'on-click)
(call-next-method))))

(defmethod on-middle-click :around ((*sketch* sketch) x y)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(propagate-to-entity *sketch* x y #'on-middle-click)
(call-next-method))))

(defmethod on-right-click :around ((*sketch* sketch) x y)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(propagate-to-entity *sketch* x y #'on-right-click)
(call-next-method))))
(defmacro def-xy-event-method (method-name)
`(defmethod ,method-name :around ((*sketch* sketch) x y)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(propagate-to-entity *sketch* x y #',method-name)
(call-next-method)))))

(def-xy-event-method on-click)
(def-xy-event-method on-middle-click)
(def-xy-event-method on-right-click)
(def-xy-event-method on-click-press)
(def-xy-event-method on-middle-click-press)
(def-xy-event-method on-right-click-press)
(def-xy-event-method on-hover)

(defmethod on-hover :around ((entity entity) ix iy)
(let ((*draw-mode* nil))
Expand All @@ -57,14 +56,17 @@

(defmethod kit.sdl2:mousebutton-event ((instance sketch) state timestamp button x y)
(let ((button (elt (list nil :left :middle :right) button))
(method (elt (list nil #'on-click #'on-middle-click #'on-right-click) button)))
(click-method (elt (list nil #'on-click-press #'on-middle-click-press #'on-right-click-press) button))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey! thanks for making the changes! I'm confused here now --- it's saying click methods are the press ones, and release methods are .. click?

why don't we do all three methods - press, click, release? I feel like release is interesting in a drag & drop scenario

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Click methods are release, ya. That's how it works in JavaScript (onclick()) and I think it matches people's intuitions. E.g. if you press down the mouse button and move it around, you don't expect the click to take effect until you release the button. Could add an explicit on-click-release but it would be doing the same thing as on-click.

(release-method (elt (list nil #'on-click #'on-middle-click #'on-right-click) button)))
(when (equal state :mousebuttondown)
(setf (getf *buttons* button) t))
(setf (getf *buttons* button) t)
(funcall click-method instance x y))
(when (and (equal state :mousebuttonup) (getf *buttons* button))
(setf (getf *buttons* button) nil)
(funcall method instance x y))))
(funcall release-method instance x y))))

(defmethod kit.sdl2:mousemotion-event ((instance sketch) timestamp button-mask x y xrel yrel)
(on-hover instance x y)
(unless
(loop for entity being the hash-key of (sketch-%entities instance)
for (im iw ih) being the hash-value of (sketch-%entities instance)
Expand Down Expand Up @@ -99,5 +101,31 @@

;;; Keyboard

(defmethod keyboard-event :after ((instance sketch)
state timestamp repeatp keysym))
(defconstant +scancode-prefix-length+ (length "scancode-"))

(defmethod on-text (instance text))
(defmethod on-key (instance key state))

(defmethod on-text :around ((*sketch* sketch) text)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(call-next-method))))

(defmethod on-key :around ((*sketch* sketch) key state)
(with-sketch (*sketch*)
(let ((*draw-mode* nil))
(call-next-method))))

(defmethod kit.sdl2:textinput-event :after ((instance sketch) timestamp text)
(on-text instance text))

(defmethod kit.sdl2:keyboard-event :after ((instance sketch) state timestamp repeat-p keysym)
(when (not repeat-p)
(on-key instance
;; Removing the ugly "SCANCODE-" prefix from the keyword
;; symbol that denotes the button.
(intern (subseq (symbol-name (sdl2:scancode keysym))
+scancode-prefix-length+)
(find-package "KEYWORD"))
state)))

5 changes: 5 additions & 0 deletions src/package.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,14 @@
:on-click
:on-middle-click
:on-right-click
:on-click-press
:on-middle-click-press
:on-right-click-press
:on-hover
:on-enter
:on-leave
:on-text
:on-key

;; Control flow
:start-loop
Expand Down