Espresso

Lightweight wrapper around GLFW for Crystal. Provides an OOP and "Crystal-like" interface to GLFW.

Installation

  1. Add the dependency to your shard.yml:
dependencies:
  espresso:
    gitlab: arctic-fox/espresso
  1. Run shards install

  2. Make sure you have GLFW 3.3 installed to your system.

Install GLFW 3.3

Download the packages from GLFW's website or compile from source.

If you're on Linux or MacOS, and want to build from source, run ./install-glfw.sh with sudo in this directory. You will need CMake, git, and a C compiler installed. You will probably also need Xorg development libraries. On Ubuntu, those can be installed with apt-get install xorg-dev.

Usage

Here's an example of displaying a window:

require "espresso"

Espresso.run do
  Espresso::Window.open(800, 600, "Espresso") do |window|
    until window.closing?
      window.swap_buffers
      Espresso::Window.poll_events
    end
  end
end

GLFW must be initialized before most of its operations can be performed. The best way to do this is by using Espresso.run, like so:

Espresso.run do
  # Use GLFW here.
end

GLFW will be initialized before calling the block, and terminated after the block is done.

Alternatively, Espresso.init and Espresso.terminate can be used, but you are responsible for ensuring they get called correctly.

Most of the functions from GLFW are wrapped by instance methods. They are placed into a type that corresponds with their purpose.

For example, functions in GLFW that would normally take a monitor pointer as the first argument, are now instances methods in a Monitor struct.

monitor = Monitor.primary
puts monitor.name

is equivalent to the C code:

GLFWmonitor *monitor = glfwGetPrimaryMonitor();
printf("%s\n", glfwGetMonitorName(monitor));

Additionally, the pointer is the only instance variable in the struct.

When compiled, this indirection is removed, so it's as fast as calling the method itself. This provides a friendlier object-oriented approach without sacrificing speed.

Windows

Windows are the biggest part of GLFW. As such, there is a lot of functionality put behind them. The easiest way to get a window in Espresso, is to call Window.open or Window.full_screen. A block can be provided to these methods. When present, Espresso will automatically make the window's context current and ensure proper cleanup of its resources. Without a block, use Window.new or Window.full_screen. These methods simply return a Window instance.

# For windowed mode.
Espresso::Window.open(800, 600, "Espresso") do
  # Use the window here.
end

# For full-screen mode.
Espresso::Window.full_screen("Espresso") do
  # Use the window here.
end

# Alternatively, without the block form:
window = Espresso::Window.new(800, 600, "Espresso")
# or for full screen...
window = Espresso::Window.full_screen("Espresso")
# Make sure to set the context and destroy when done.
window.current!
window.destroy!

You may want to customize the window before creating it. To do so, use WindowBuilder. Use one of the build_* methods to create the window.

builder = Espresso::WindowBuilder.new
builder.context_version(3, 3)
builder.resizable = false
window = builder.build(800, 600, "Espresso")

Input

Most input types are tied to GLFW windows. To access them, use Window#mouse and Window#keyboard. For joystick input, use the Joystick type, since it isn't tied to a window.

Events

If you're familiar with how GLFW handles events, you'll know that it uses callbacks like glfwSetKeyCallback. However, Espresso changes how event callbacks are exposed. Instead of setting a single callback for an event, Espresso allows setting multiple. Additionally, native blocks and closures can be used (which isn't allowed with normal C callbacks). Registering an event listener in Espresso is as easy as passing a block to any #on_* method.

window.keyboard.on_key do |event|
  # The `event` argument contains all event information.
  puts "Key #{event.pressed? ? "pressed" : "released"} #{event.key}"
end

To remove a callback at a later point in time, call the corresponding #remove_*_listener. The #on_* method returns a proc, which needs to be passed to the #remove_*_listener. Removing a listener is optional - they will automatically be cleaned up when the resource they're tied to is destroyed.

proc = window.keyboard.on_key do |event|
  # ...
end

# ...

window.keyboard.remove_key_listener(proc)

Events are typically tied to an instance, but some events aren't (or can't be) tied to an instance. Those exceptions are the #on_connect events for monitors and joysticks. Listeners can be set up for disconnect of all monitors and joysticks, or just one instance.

Espresso::Monitor.on_connect do |monitor|
  if event.connected?
    # New monitor connected.
  else
    # Monitor disconnected.
  end
end

# The instance-specific variant.
# This is only invoked for the monitor instance it is associated with.
monitor = Espresso::Monitor.primary
monitor.on_disconnect do |monitor|
  # Called when the primary monitor is disconnected.
end

Errors

GLFW errors have been changed to exceptions in Espresso. All calls that could possibly cause an error are wrapped and checks handled by Espresso. If GLFW reports an error, it will be raised from within Espresso (as to not break out of the stack). All errors inherit from a base GLFWError class.

begin
  window.resize(800, 600)
rescue ex : Espresso::PlatformError
  # Handle error.
end

Documentation

Documentation is automatically generated and published here. The primary pages you will be interested in are:

Development

Ameba is used for linting. The CI build ensures that proper formatting and style is applied.

A docker image is generated to run tests in. It runs Ubuntu 18 and installs Crystal, GLFW, and everything else needed for simulating an environment. Specs are run in the docker container as part of the CI build. Due to the difficulty, writing tests is not required, but encouraged if it can be done. Specs might not pass on your system, but should pass in the docker container.

Contributing

  1. Fork it (https://gitlab.com/arctic-fox/espresso/forks/new)
  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 Merge Request

Contributors