- Common Lisp 100%
|
All checks were successful
CI / test (push) Successful in 1m37s
To the latest commits of cl-ffi, whose allocate boxes no pointer, and of the bindings that pin it, of Pyssel's step 25. |
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| tests | ||
| vulkan | ||
| wayland | ||
| win32 | ||
| .gitattributes | ||
| .gitignore | ||
| cl-windowing.asd | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.org | ||
| stiftfile | ||
cl-windowing
- Status
- Windows and events
- Input
- Popups
- Dialogs
- Input methods
- Cursors
- Holding the pointer
- The clipboard
- Fullscreen
- Resizing
- Scaling
- Threads
- Vulkan
- Errors and lifetimes
- Backends
- Development
- License
Native windows and input for Common Lisp on Wayland, Windows and X11, without SDL or GLFW. Vulkan-ready.
(asdf:load-system :cl-windowing/wayland) ; on Windows, :cl-windowing/win32
(defpackage #:example
(:use #:cl)
(:local-nicknames (#:win #:cl-windowing)))
(in-package #:example)
(win:with-window-system (window-system)
(win:with-window (window window-system :title "Hello" :width 800 :height 600)
(loop until (win:window-configured-p window)
do (win:process-events window-system :timeout 1))
(let ((event (win:make-event)))
(loop while (win:poll-event window-system event)
collect (list (win:event-type event)
(win:event-width event) (win:event-height event))))))
;; => ((:RESIZE 800 600))
;; On Windows, the window gets the keyboard at once:
;; => ((:RESIZE 800 600) (:FOCUS-IN 0 0))
cl-windowing talks to the platform's window system directly, through a
backend:
cl-windowing: the backend-independent API: window systems, windows and events.cl-windowing/wayland: the Wayland backend, on libwayland-client through cl-wl. Windows are xdg-shell toplevels, with server-side decorations through xdg-decoration where the compositor offers it, and dialogs marked as such through xdg-dialog.cl-windowing/win32: the Windows backend, on user32 through cl-win32. Windows have the system's title bars and borders. Needs Windows 10 version 1607 or later.cl-windowing/vulkan: Vulkan surfaces for windows, through cl-vk.
Status
Early work, on Wayland and Windows: windows, dialogs and popups, resizing by the user and by the program, fullscreen, scaling, close requests, keyboard and pointer input, confining and capturing the pointer, input methods, the pointer's cursor, the clipboard's text, and Vulkan surfaces. X11 comes next.
Windows and events
open-window-system connects to the window system with the platform's
backend, :win32 on Windows and :wayland elsewhere, or the one given as
:backend. create-window opens a top-level window; it appears once
something is drawn in it. Draw nothing before window-configured-p is
true, when the window system has given the window its first size: until
then window-pixel-size is 0 by 0.
process-events reads what the window system sends; call it regularly from
one thread, the event thread. It waits up to :timeout seconds (0 by
default, nil for no limit). poll-event copies the oldest event into an
event made once with make-event, and allocates nothing; copy-event
copies one event into another, to keep it or hand it on. Events are:
| Type | Meaning |
|---|---|
:resize |
the window system proposes a new size, event-width by event-height, or a new scale, event-scale |
:close |
the user asked to close the window, or the window system dismissed a popup |
:focus-in, :focus-out |
the window gained or lost the keyboard |
:key-press, :key-release |
a key went down, repeated, or came up |
:text |
text was typed, or chosen in an input method: event-text |
:preedit, :delete-surrounding |
an input method's work; see "Input methods" |
:pointer-enter, :pointer-leave, :pointer-motion |
the pointer came in, left, or moved: event-x, event-y, and for a motion event-dx, event-dy, how far |
:button-press, :button-release |
a button went down or up: event-button |
:scroll |
a wheel turned or a touchpad scrolled: event-scroll-x, event-scroll-y |
window-system-capabilities says what the window system can do beyond the
basics: :server-side-decorations, :clipboard, :pointer-capture, and
:keyboard and :pointer while the seat has them. Ask instead of
assuming: compositors and machines differ.
window-decorations says whether a window has decorations.
Input
Physical keys, what the layout makes of them, and the text typed are three separate things:
event-keyis the physical key, named for its place on a US keyboard whatever the layout::a,:digit-1,:left-shift,:f1,:up,:keypad-enter. Use it for things that depend on where keys are, such as WASD movement.event-symbolis the key the layout makes of it, in the same names, ornilif it is none of them. On a French keyboard the key right of Caps Lock is physically:aand logically:q. Use it for shortcuts, such as Control+Z.:textevents carry what was typed, after dead keys and compose sequences: a dead acute thenetypesé. They are not sent while Control, Alt or Super is held, because those keys make shortcuts.
event-scancode is the platform's own code for a key, for remapping keys
without names. event-repeat-p is true for a press repeated because the key
is held. The window system repeats keys at the rate the platform asks for.
(event-modifier-p event :shift) says whether a modifier was held: :shift,
:control, :alt, :super, :caps-lock or :num-lock. event-modifiers
is the mask of them all, one modifier-bit each.
Pointer positions are in the window's coordinates, from its top left.
Buttons are :left, :right, :middle, :back, :forward, or the
platform's number for others. Scrolling is in steps, positive right and
down: one notch of a wheel is 1.0. Touchpads scroll continuously, at 10 of
the window's units per step.
Text in event-text belongs to the event and is overwritten by the next
poll-event into it: copy it, or the whole event with copy-event, to
keep it.
On Wayland, input comes from the compositor's first seat, and keymaps go through cl-xkbcommon.
On Windows, keys are named by their scancodes, and the layout's virtual
keys give event-symbol; text comes as Windows translates key presses,
dead keys included. AltGr is not reported as Control and Alt, though
Windows sends it as both: it types characters, and they arrive as :text.
While a button is held, the window keeps the pointer's events, even when
the pointer leaves it; a window that loses the pointer meanwhile, as to
Alt+Tab or to a modal dialog, gets the button's release at once, since
Windows sends none.
The design follows Wayland's rules, so that every backend can meet them: there are no global window coordinates and no positioning of top-level windows, and popups are placed against a rectangle of their parent.
Popups
A popup is a window made on another, its parent, for a menu, a list to choose from or a tooltip. Windows have no places on the screen that a program can know, so a popup is placed against a rectangle of its parent, such as the button that opens it, and the window system puts it there, moving it where it would not fit on the screen:
;; A menu below its button, at 20, 10 and 80 by 24 in the window.
(win:create-popup window 160 240 :rectangle '(20 10 80 24) :grab t)
;; A submenu beside its item, in the menu.
(win:create-popup menu 160 120 :rectangle '(0 48 160 24) :anchor :top-right :grab t)
;; A tooltip centred below the pointer, which lets the pointer through.
(win:create-popup window 120 30 :rectangle (list x y 0 0) :anchor :bottom
:popup-anchor :top :offset-y 16 :pointer-input nil)
:anchor is the point of the rectangle and :popup-anchor the point of
the popup put there: corners such as :bottom-left, the middles of
edges such as :top, or :center. By default the popup's top left goes
at the rectangle's bottom left, so it opens below. Where it would not fit,
the window system moves it as :adjustments allow: by default it flips to
the other side of the rectangle, then slides along the screen's edge;
:resize-x and :resize-y let it shrink. popup-position says where it
went, in the parent's coordinates. The rectangle, of the type rectangle,
is a list (x y width height), rounded out to whole units and cut to the
parent's size.
A popup is a window like any other, with a size, a scale and events of its
own: draw nothing before window-configured-p is true. Its size can
change as it is placed, with a :resize event; reposition-popup places
it again, at another size or against another rectangle.
A popup that grabs, with :grab t, gets the keyboard while it is open,
and the window system dismisses it when the user clicks outside the
program's windows or switches to another program: it gets a :close
event, popup-dismissed-p is then true, and it should be destroyed. Make
one in answer to a button or key press. Popups that grab form one chain:
each is made on the newest one that grabs, or the first on a top-level
window while none grabs, and popup-error is signalled otherwise, so
close a menu before opening another elsewhere. Popups that do not grab
may be made on any window.
destroy-window destroys a window's popups first, the newest first, as
Wayland requires, and window-popups lists them. The parent must have
been drawn before a popup is made on it.
A program that draws popups itself, inside one of its windows, as a game
in fullscreen draws its menus, places them by the same rules with
place-rectangle, within the window rather than the screen:
(win:place-rectangle 200 300 '(0 0 640 480) :rectangle '(100 450 80 24))
;; => 100, 150, 200, 300: no room below the rectangle, so above it
On Wayland popups are xdg-shell popups, placed by the compositor; a popup
that grabs takes the grab with the serial of the latest key or button
press. Moving one with reposition-popup needs xdg_wm_base version 3,
which every current compositor offers. On Windows popups are windows
without frames owned by their parent's window, never activated; the
backend places them on the work area of the display, and places them
again as their parent moves. A popup that grabs gets the keys typed while
the program has the focus, and is dismissed when the focus goes
elsewhere or the user presses a button on a title bar. Input methods do
not compose in popups on Windows.
Dialogs
A dialog is a top-level window made on another, its parent, as one that
asks which file to open. create-window makes one with :parent: it
stays above its parent, opens over it, and is destroyed with it. With
:modal t too, the user answers it before going back to the parent:
(let ((dialog (win:create-window window-system :title "Open" :width 480 :height 320
:parent window :modal t)))
;; Draw it and handle its events until the user answers, then:
(win:destroy-window dialog))
While a modal dialog is open, its parent and the parent's popups get no
input that starts there: no key presses, text, button presses, scrolls,
pointer motion or :close requests. Releases still come, so a key or
button held when the dialog opened is released, and so do :focus-in,
:focus-out and the pointer's coming and going. window-parent,
window-modal-p and window-dialogs say how windows are related.
dialog-error is signalled for a modal window without a parent, and for
a parent that is a popup or belongs to another window system.
On Wayland a dialog is an xdg-shell toplevel given its parent, marked as a dialog, modal or not, through xdg-dialog where the compositor offers it. The compositor decides where it goes and how it looks, and learns of the parent only once the parent has been drawn. On Windows a dialog is a window owned by its parent's window, with no button on the taskbar, opened over the middle of its parent. A modal one disables its parent's window while it is open, as Windows' dialog boxes do: Windows then shows the dialog when the user clicks the parent.
Input methods
An input method composes text from what is typed, as for Chinese and Japanese, and the user chooses the result from its candidates. A text field takes text from one while it has the focus:
(win:start-text-input window) ; the field gains the focus
(win:set-text-input-caret window x y 2 line-height) ; its caret, in window coordinates
(win:set-text-input-surrounding window line cursor) ; the text around the caret
…
(win:stop-text-input window) ; the field loses the focus
While it does, the input method's work arrives as events, in the order to apply them:
| Type | Meaning |
|---|---|
:delete-surrounding |
delete event-delete-before characters before the caret and event-delete-after after it |
:text |
the text the user chose, to insert at the caret, as typing's |
:preedit |
the text being composed, event-text, to show at the caret, underlined and apart from the field's text, until the next :preedit replaces it; "" when nothing is composed. event-caret-start and event-caret-end are the input method's caret in it, or nil |
Give the caret and the surrounding text again whenever they change, as after handling these events; unchanged ones cost nothing, so a text field may give them every frame. On Wayland input methods speak text-input-v3, where the compositor offers it. On Windows they speak IMM32: a window's input method is off while it takes no text input, so keys reach it as they are typed, and Windows' input methods are not told the surrounding text.
Cursors
Each window has a cursor, the pointer's look while it is over the window, named as CSS names cursors:
(setf (win:window-cursor window) :text) ; an I-beam, over text
(setf (win:window-cursor window) :ew-resize) ; over a divider
(setf (win:window-cursor window) :hidden) ; no cursor at all
It is :default, the arrow, until set, and takes effect at once if the
pointer is over the window. Setting the cursor a window has already costs
nothing, so it may be set on every pointer motion. The type cursor lists
them all: :pointer for links and buttons, :wait and :progress for
busy, the resize cursors for edges and corners (:n-resize, :nwse-resize
and so on) and for columns and rows, the drag and drop cursors, and more.
On Wayland the compositor draws the cursor from its name where it offers
cursor-shape-v1. Elsewhere cl-windowing draws it from the user's cursor
theme, named by $XCURSOR_THEME and $XCURSOR_SIZE as other toolkits read
them, through libwayland-cursor, at the window's scale, and animates
animated cursors. A cursor the theme lacks is shown as :default. On
Windows the cursors are the user's system cursors; where Windows has none
for a cursor, the nearest it has is shown, such as the arrow for
:zoom-in.
Holding the pointer
A window holds the pointer as its pointer mode says:
(setf (win:window-pointer-mode window) :captured) ; a camera follows the mouse
(setf (win:window-pointer-mode window) :confined) ; a strategy game's edge scrolling
(setf (win:window-pointer-mode window) :free) ; as usual, the default
:confined keeps the pointer within the window. :captured hides it and
keeps it where it is, and its motion comes as :pointer-motion events
whose event-x and event-y stay put and whose event-dx and event-dy
are the device's own motion, without the platform's acceleration: about a
unit for each count of the mouse, so a camera turns by them times a
sensitivity. A window holds the pointer only while it has the focus and
the pointer has come over it, and gives it back while another window or
program has the focus, so the mode lasts until it is set again; a game
sets it once, as play starts, and again on pausing.
window-system-capabilities includes :pointer-capture where the window
system can hold the pointer; setting another mode than :free signals
unsupported-error where it cannot. On Wayland the compositor holds the
pointer, through pointer-constraints-v1, and the captured pointer's
motion comes through relative-pointer-v1; the compositor must offer both.
On Windows a window keeps the pointer within its client area, or at one
point of it, with ClipCursor, and the captured pointer's motion is the
mouse's raw input.
The clipboard
clipboard-text returns the text on the clipboard, or nil, and setf
puts text there, for this and other programs to paste:
(setf (win:clipboard-text window-system) "Copied") ; on Control+C
(win:clipboard-text window-system) ; on Control+V
Lines end with #\Newline whatever the platform uses. The text may be
another program's, which is asked for it: clipboard-text waits up to
:timeout seconds, 1 by default, and returns nil if that program does
not answer. Do both in answer to the user: on Wayland a program sees the
clipboard only while one of its windows has the keyboard focus, and
setting it then signals window-system-error otherwise.
window-system-capabilities includes :clipboard where there is one.
On Wayland, the text stays on the clipboard until something else is put
there or the window system is closed. On Windows it stays until
something else is put there; while the session is locked, Windows gives
no program the clipboard, and both signal window-system-error.
Fullscreen
(setf (win:window-fullscreen-p window) t) ; on F11, say
(setf (win:window-fullscreen-p window) nil) ; and back
A fullscreen window covers the display it is on, without decorations,
and the window system sizes it: its size comes as a :resize event, as
any other, and set-window-size asks nothing while it lasts. The window
system decides: window-fullscreen-p says what it did, once it has. Back
from fullscreen, the window is as it was. A popup is never fullscreen.
On Wayland the compositor makes the window fullscreen on the output it
chooses, and says so in a configure, so window-fullscreen-p changes as
events are processed. On Windows a fullscreen window is a window without
a frame covering its display, at once; leaving fullscreen puts it back
where it was, maximised if it was.
Resizing
A resize is a handshake between the window system and the code that
renders. The window system proposes a size with a :resize event, and
window-pending-size returns it until it is taken. The renderer recreates
its swapchain at that size, then calls adopt-window-size just before
presenting the first frame at it. The window system is told only then that
the next frame follows its proposal, so it never sees a frame that
disagrees. window-size is the size last adopted.
The program asks for a size with set-window-size, in units, and the
size comes the same way, with a :resize event:
(win:set-window-size window 1280 720)
;; => T
The window system may give the window another size, as one that fits the
screen. While it sizes the window itself, nothing is asked and
set-window-size returns nil: on Windows while the window is maximised,
minimised or fullscreen, and on Wayland while it is maximised, fullscreen or
tiled, or the user is resizing it. On Wayland a window sizes itself by drawing
at the size, which xdg-shell allows, so a configure that comes before the
compositor has seen a frame at it, and proposes the old size, does not undo
it. A popup keeps its placement, as reposition-popup with only a size places
it.
Scaling
A window's size is in its own coordinates, which pointer positions use
too. Its scale, window-scale, is how many pixels its frames have to a
unit of them on the display it is on: an exact rational such as 1, 2 or
3/2 for a display scaled to 150%. window-pixel-size is the size of its
frames in pixels, size times scale. Draw at that size, and draw things at
their size in the window's coordinates times the scale, so they look the
same size on every display.
A new scale comes as a resize does: a :resize event with event-scale,
window-pending-pixel-size until it is taken, and adopt-window-size just
before the first frame drawn at it.
On Wayland, fractional scales come from fractional-scale-v1, and frames are
shown at the window's size through viewporter, where the compositor offers
both; window-system-capabilities then includes :fractional-scale.
Otherwise the scale is a whole number: the compositor's preferred buffer
scale for the window, or, from a compositor whose wl_surface is older than
version 6, such as Weston 16, the largest scale of the outputs the window
is on, displays plugged in later included.
On Windows, a window's scale is the DPI of the display it is on over 96:
3/2 at 150%. Windows sizes windows in pixels, so a window's size in its
coordinates is its pixel size over its scale, rounded; at a fractional
scale, window-pixel-size can then differ from the window's real pixel
size by a pixel, which the swapchain takes from Windows. A window moved to
a display of another scale keeps its size in its coordinates.
Threads
process-events may be called from one thread at a time, the event
thread; everything else may be called from any thread. adopt-window-size
may run on the render thread while process-events waits on the event
thread: a window system serialises access to the platform with a lock,
which process-events does not hold while it waits.
On Windows, a window gets its messages only on the thread that created it.
So each window system runs a thread of its own, which creates the windows
and handles their messages, and create-window and destroy-window are
handed to it from whatever thread calls them. Its events wait for
process-events as on Wayland. While the user drags a window's border,
Windows runs a loop of its own in that thread, and events keep coming.
Vulkan
(vk:with-instance (instance :extensions (cl-windowing/vulkan:vulkan-instance-extensions
window-system))
(let ((surface (cl-windowing/vulkan:create-vulkan-surface window instance)))
...))
vulkan-instance-extensions names the instance extensions the window
system's surfaces need. create-vulkan-surface returns a cl-vk:surface;
size its swapchain by window-pixel-size and window-pending-pixel-size,
call adopt-window-size before presenting at a new size, and destroy the
surface before the window.
Errors and lifetimes
Errors are windowing-error conditions:
window-system-error: the window system cannot be reached, or the connection failed.no-backend-error: the backend asked for, or the platform's, is not loaded.popup-error: a popup breaks the window system's rules, as a popup that grabs made where another grabs already.dialog-error: a dialog cannot be made, as a modal window without a parent.unsupported-error: the window system cannot do what was asked, as capturing the pointer wherewindow-system-capabilitieslacks:pointer-capture.closed-error: a closed window system or a destroyed window was used.
destroy-window and close-window-system release what create-window and
open-window-system made, and with-window and with-window-system do it
on leaving. Releasing succeeds even when the connection has failed, so the
error that broke it is the one that leaves the with- forms.
Backends
A backend subclasses window-system, window and popup, registers
itself with define-backend, and implements the backend- generic
functions. It reports what the platform sends with window-configured,
window-close-requested, window-decorations-changed,
window-fullscreen-changed, popup-configured and popup-dismissed, and
input with queue-event, whose modifier masks
modifier-bit makes, and which drops the input a modal dialog keeps from
its parent. A backend whose platform does not place popups
itself places them with place-popup, which follows xdg_positioner's
rules, within the part of the screen windows may cover. evdev-key and
evdev-button name the Linux kernel's key and button codes, which the
Windows backend maps scancodes to as well. size-in-pixels is how a size
in units becomes pixels.
The core calls backend-create-window, backend-create-popup,
backend-reposition-popup, backend-destroy-window, backend-set-title,
backend-set-window-size, backend-set-fullscreen,
backend-pointer-mode-changed, backend-show-cursor,
backend-text-input-changed, backend-clipboard-text,
backend-set-clipboard-text and backend-close without the window
system's lock, so that a backend can hand them to a thread of its own and
wait; they take the lock themselves around what they share, with
with-window-system-lock, as code that reports events does. The other
backend- functions are called holding it. backend-create-window gets
a dialog's :parent and :modal, which the window is made with as
:parent and :modal-p. A backend whose platform leaves a window to
size itself, as Wayland does, reports the size
backend-set-window-size asks for with window-configured and a serial
of nil: there is nothing to acknowledge. The Wayland backend in
wayland/ is the example to follow; the Win32 backend in win32/ shows a
backend with a thread.
Development
Dependencies are pinned in the stiftfile with
stift, and tests use
parachute.
stift test
The suite tests the backend of the platform it runs on.
On Windows, the tests open real windows, briefly, so they need a desktop session, and a Vulkan driver for the Vulkan tests. They play the user by sending the windows the messages Windows would send.
Elsewhere, the protocol tests run against cl-wl's in-process compositor, so
they need only libwayland (Debian/Ubuntu: libwayland-client0
libwayland-server0).
The Vulkan tests start headless Weston (weston) and present to it, so
they need a Vulkan driver that presents to Wayland, such as Mesa's lavapipe
(libvulkan1 mesa-vulkan-drivers). When the Khronos validation layer is
installed (vulkan-validationlayers), they run under it and fail on any
error it reports.
License
MIT. See LICENSE.