wayland_client

A Wayland client for Crystal, intended to handle the nitty gritty details of interacting with Wayland, while interacting well with the Crystal event loop. It does this by providing reasonably ideomatic interfaces that make it easy to accomplish what often takes a bunch of instructions and/or callback setup and nonobvious setup to do directly.

This is however not a graphics toolkit - this provides the buffers to write to, nothing more. It does not provide any functionality of rendering text, for example. You get a buffer, and it is up to you to fill it with something reasonable.

Installation

  1. Install needed libraries.

This means that the following libraries need to be present

libdecor
libwayland-client
libxkbcommon
libdecor

Also for obvious reasons, please remember that for things to work Wayland needs to be used on the system.

  1. Add the dependency to your shard.yml:

    dependencies:
      wayland_client:
        github: yxhuvud/wayland_client
  2. Run shards install

Warning: So far it has only been tested with Gnome. Please report (or send fixes) for Plasma or other wayland compositors. Supporting decorations using the extension instead of through libdecor is a major task, but would be accepted as long as the libdecor stuff also works. The latter is necessary on Gnome because they don't want to support the extension.

Usage

See the examples folder for more details and how things tie together.

Client

The client is the main way to create and setup things. It also handle the event loop interaction. To actually do something, you need a main surface that you are using. After creating that, then you need to create a frame for it do actually display it. That frame taka a block that will be invoked when the frame is ready to display, or when the window is resized or generally when the compositor feels like it.

When the block is invoked the surface needs to be filled with content and committed. We also need to mark that the whole buffer is damaged and needs to be repainted. To do that we need to check out a buffer and then fill that buffer with data.

Resizing the buffers are handled by the framework, so don't keep references around to buffers or whatnot.

Anyhow, once all the things are set up, client.wait_loop needs to be called to actually display things and to handle incoming events. This call will block until the frame is closed, so if you want things to happen in the background you should create new fibers for that.

Example:

WaylandClient.connect do |client|
  surface = client.create_surface(
    kind: :memory,
    format: WaylandClient::Format::XRGB8888,
    opaque: true,
  )
  frame = client.create_frame(surface, title: "hello", app_id: "hello app") do |max_x, max_y, window_state|
    # `repaint!` is a shortcut to do all of this except
    # filling the buffer (which it yields to a block)
    buf = surface.attached_buffer
    buf.map! { WaylandClient::Format::XRGB8888.new(0xFF, 0xFF, 0xFF) }
    buf.damage_all
    buf.commit
    surface.commit
  end

  client.wait_loop
  surface.close
end

Relevant methods:

.connect: Create a client. Takes a block and yields the client. Disconnects when done.

wait_loop: Wait for events to come in, and then handles them. Blocking.

pointer: listener handler for handling mouse pointer input. Has one relevant method, the handler= method. See <examples/complex.cr> for how to do that.

keyboard: listener handler for handling keyboard input. Has one relevant method, the handler= method. See <examples/complex.cr> for how to do that.

touch as above, except it is actually a big TODO so far. PRs welcome.

create_surface(kind : Buffer::Kind, format, opaque, accepts_input = true)

Creates a new surface. Needed to create a new frame.

create_frame(surface, title = nil, app_id = nil, &block). Creates a Frame. Takes a surface, a title (that is shown in the title bar), and an app_id (which is shown in the task bar), and a block. The block needs to be there and will need to check out a buffer for the surface, and then commit the surface. If no surface is painted and committed then no window will appear, so don't forget this part.

Frame

Frames define the windows that are painted on the screen. Contain functions related to handling said window. Created from the Client.

On a technical level these are backed by a library called libdecor, because I don't want to reinvent basic stuff like window decorations.

Relevant methods: #visibility= Turns this frame visible or invisible

#visible? Is this frame visible?

#title= Set frame title

#title Get frame title

#app_id= Set frame app_id, ie how it is shown in the task bar.

#close Closes the frame

#fullscreen Set window to fullscreen. Note: This is quite buggy and full of sharp corners. Needs polish.

#unfullscreen Reset fullscreen status. Note: Doesn't work very well. Needs polish.

#movable? Can the frame be moved?

#movable= Toggle frame movability

#minimizable? Can the frame be minimized?

#minimizable= Toggle frame minimizability

#closable? Can the frame be closed (using the X)?

#closable= Toggle if X should be shown or not

#fullscreenable? Can the frame be fullscreened from the frame

#fullscreenable= Toggle if the frame can be fullscreened or not

#resizable? Can the frame be resized?

#resizable= Toggle resizability

Surface

Surfaces are used to be the thing holding buffers and represent a part of your screen. In addition to buffers, they can accept input, and are a big part of interacting with the wayland event loop. Every time you draw a new frame you need to tell the compositor what parts of a surface it needs to paint, using something that is called "damage". Surfaces have a two dimensional size, which in the case of the toplevel surface that is connected to the frame, will be defined by the frame when the user resizes the window.

Relevant methods:

Buffer

Buffers, that contain the actual graphics data. Under the hood, this will use a pool of buffers, and keep track of what the compositor is done with, and the pool will also delay updates when things get to busy on the compositor side to keep up with the monitor frame rate. Do not keep references to buffers around, always check out a new one from the surface you want to modify.

Has a bunch of methods, but the ones that should be used from the outside is.

PRs welcome for better API, this is decidedly very limited :)

Format

Binary representation formats for how pixels are stored in memory. Common ones are WaylandClient::Format::ARGB8888 and WaylandClient::Format::XRGB8888.

Each format has its own accessor methods, the ones for the ones listed above are red, green, blue, as well as alphafor the ARGB8888 format.

In addition to that there is one, cursor that is located there due to type declaration limitations/bugs in Crystal.

Input

Input is handled by assigning a handler to the corresponding input method. Each kind have their own base handler type that should be used. They each have a module, but be aware they declare an initialize, so be sure to call super() to make certain things are initialized properly.

All of these keep track of what surface they happen on. Currently there is no good way of mapping that backwards to the surface object they correspond to.

See examples/complex.cr for a simple example of how these can be used.

Mouse

Include include WaylandClient::PointerHandler into a class and have it define enter, leave, or frame. The handler include a pointer_event method that contain all information you should need to handle the user action. enter and leave are obvious what they do, but not frame. There is a bunch of other events that are emitted, but most are really multi-shot things that will be aggregated into the frame callback.

It may be that more callback methods are necessary. They will be added if necessary. I have not found any documentation for what events are aggregated into frame events and what that are not.

Keyboard

Very similar to the PointerHandler. enter and leave here is a bit less obvious but they are basically focus events. In addition to that there is to methods that can be defined, they are key and repeat_info. They have signatures:

Touch

Mostly todo as I havn't bothered setting up the laptop to test with. PRs welcome.

Counter

Premade functionality to do things like frame rate counting. Set up like

frame_counter = WaylandClient::Counter.new("Frames: %s")
spawn do
  loop { frame_counter.measure { |value| puts "frame: %s" % value } }
end

and then make sure it is called like

frame_counter.register time

whereever you want to measure.

Will likely grow more features like percentile handling at some point.

GPU usage

It should be possible to use existing Crystal libraries for OpenGL for interacting with the raw buffers. That will involve an extra copy though, so it will not be optimally efficient.

Wishlist

PRs/examples are very welcome.

Development

TODO Write development instructions here

Contributing

  1. Fork it (https://github.com/yxhuvud/wayland_client/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors