2 Control Input
dxtr edited this page 2026-10-20 02:06:16 +02:00

Control Input

The pointer's events go to the control under the pointer, and the keys to the one with the keyboard focus, as in Godot; a control may drag what it shows and take what is dropped on it.

  • Input: a control's class handles its input in a gui-input method. The pointer's events go to the topmost control under the pointer, in the control's coordinates, and on up to its parents as control-pointer-filter says: :stop, the default, keeps them from its parents, and from unhandled-input; :pass, a container's default, gives its parent those it does not handle; :ignore lets them through to what is under it. A control that takes a button press gets the pointer's events until every button is up, wherever the pointer goes. The control under the pointer hears pointer-entered and pointer-exited, and the window shows its control-cursor, such as :text or :pointer. A control of your own counts double and triple clicks with count-click, in a make-clicks of its own, as the lists do.
  • Focus: keys, text and an input method's events go to the control with the keyboard focus, and on up to its parents until one handles them. A control takes the focus as control-focus-mode allows: :none, the default; :click, when the left button presses it; :all, also when Tab, Shift+Tab or an arrow key moves the focus to it. Tab goes through the tree's order; an arrow goes to the nearest control on its side. These are Godot's built-in actions, :ui-focus-next, :ui-focus-prev, :ui-left and the rest, matched exactly, which a game may bind to other keys (see Input and Actions), as every control takes its keys: a button :ui-accept, a popup and a dialog :ui-cancel, a list and a tree the arrows', page and select actions, a text field the :ui-text- ones, the clipboard's and history's, each as Godot's control of the kind takes them. grab-focus, release-focus, control-has-focus-p and focus-owner do the rest, and a control hears focus-entered and focus-exited. A click where no control is takes the focus away, so the keys go back to the game. Each of these callbacks is a signal of the same name too, gui-input with the event.
  • Drag and drop within a window, as Godot's: a press that moves (drag-threshold) units, 10 unless set, asks the control pressed, and on up through its parents as their pointer filters pass the pointer on, for what it drags, in its control-drag-data method; the first that gives something starts a drag of it, and set-drag-preview has a control follow the pointer meanwhile. The control under the pointer, and on up, is asked with can-drop-data-p whether it takes it, the window showing :grabbing over one that does and :no-drop over one that does not, and gets it in drop-data as the button comes up. :ui-cancel, Escape, or the right button cancels. dragging-p, dragged-data and drag-successful-p tell how it goes, every control of the window hears drag-began and drag-ended, and force-drag begins a drag from code. Tab bars and tab containers move tabs by dragging with :drag-to-rearrange-enabled t, between those of one :tabs-rearrange-group too; a tree shows where a drag would drop as tree-drop-mode-flags asks; and a line edit's selection drags as text.

A control takes input in its class's gui-input method. Turn the tiles of the grid of Containers into lamps, which light up and go out as they are clicked, or as Space is pressed while one has the keyboard focus. A lamp draws the style box its state names, :on or :off, from the theme the grid holds:

(sc:define-node lamp (ui:panel)
  ((lit :initform nil :accessor lamp-lit :type boolean)))

(defmethod s2d:draw ((lamp lamp) canvas)
  (ui:draw-style-box (ui:control-style-box lamp (if (lamp-lit lamp) :on :off)) canvas
                     0 0 (ui:control-width lamp) (ui:control-height lamp)))

(defmethod ui:gui-input ((lamp lamp) event)
  (when (or (and (typep event 'sc:button-event) (sc:event-pressed-p event)
                 (eq (sc:event-button event) :left))
            (and (typep event 'sc:key-event) (sc:event-pressed-p event)
                 (eq (sc:event-key event) :space)))
    (setf (lamp-lit lamp) (not (lamp-lit lamp))
          (sc:event-handled-p event) t)
    (s2d:queue-redraw lamp)))

(sc:call-in-tree *tree*
                 (lambda ()
                   (let ((grid (part *tree* "layout/columns/grid")))
                     (setf (ui:control-theme grid)
                           (make-instance 'ui:theme
                                          :style-boxes `((lamp :off ,(rounded '(0.6 0.45 0.3))
                                                               :on ,(rounded '(1 0.85 0.4))))))
                     (dolist (tile (sc:node-children grid))
                       (sc:queue-free tile))
                     (dotimes (index 9)
                       (add grid (make-instance 'lamp :custom-minimum-height 60
                                                      :expand-horizontal t
                                                      :focus-mode :all :cursor :pointer))))))

Over a lamp the pointer is a hand. A click lights it and gives it the focus, which Tab and the arrow keys then move from lamp to lamp, and Space lights the lamp that has it.

A control class of your own drags and drops with three methods. Paint the tiles of the docks of Containers, which drag their colours and take colours dropped on them:

(sc:define-node paint (ui:panel) ())

(defun paint (color &rest initargs)
  (apply #'make-instance 'paint :style-box-overrides (list :panel (rounded color)) initargs))

(defun paint-color (paint)
  (ui:style-box-background-color (ui:control-style-box paint :panel)))

(defmethod ui:control-drag-data ((paint paint) x y)
  (declare (ignore x y))
  (ui:set-drag-preview paint (paint (paint-color paint) :offset-right 24 :offset-bottom 24))
  (paint-color paint))

(defmethod ui:can-drop-data-p ((paint paint) x y data)
  (declare (ignore x y))
  (pyssel/2d:colorp data))

(defmethod ui:drop-data ((paint paint) x y color)
  (declare (ignore x y))
  (setf (ui:control-style-box paint :panel) (rounded color)))

(sc:call-in-tree *docks*
                 (lambda ()
                   (let ((flow (part *docks* "docks/flow")))
                     (dolist (tile (sc:node-children flow))
                       (sc:queue-free tile))
                     (dolist (color '((0.8 0.3 0.3) (0.3 0.7 0.4) (0.3 0.4 0.8) (0.9 0.8 0.3)))
                       (add flow (paint color :custom-minimum-width 80 :custom-minimum-height 60))))))

Drag a tile onto another: a square of its colour follows the pointer, the pointer shows whether the control under it takes the colour, and the tile under it takes it as the button comes up. Escape or the right button cancels the drag.