The window
Learn how to create and configure a window
Creating a window
As you saw in the “get started” tutorial, the simplest thing you can do in Ruby 2D is require the gem and show the window:
require 'ruby2d'
show
If you’d like to follow along, save this code to a file named window.rb and run it using the standard Ruby interpreter on the command line, like so:
ruby window.rb
You should see a black, empty window with a size of 640-by-480 pixels and a title bar with the text “Ruby 2D”.
Setting attributes
When you don’t set any window attributes, the default values will be used. You can change these attributes by using the set method. Let’s try changing the window title before we show it:
require 'ruby2d'
set title: "Hello World!"
show
Notice the title bar of the window is now set to the new text we provided. The Ruby 2D domain-specific language (DSL) makes it easy to change things in a natural and intuitive way. Here, we’re calling a method called set and passing it a Hash with a Symbol called :title as the key, and a String with the text "Hello World!" as the value.
Let’s play with some other attributes. The black background is a little boring, so let’s change it! We can set it to something more interesting, like the color blue:
set background: 'blue'
Try some other colors, like red, orange, lime, fuchsia, or roll the dice with random. When there are multiple attributes we want to set, we can chain them together for convenience:
set title: 'Howdy', background: 'navy'
Great! You’ve got the basics of setting attributes down. Here are all the attributes you can set:
| Attribute | Type | Default | Description |
|---|---|---|---|
title |
String | 'Ruby 2D' |
Window title bar text |
width |
Integer | 640 |
Window width in pixels |
height |
Integer | 480 |
Window height in pixels |
background |
Color | Black | Window background color |
icon |
String | nil |
Path to a window icon image |
fps_cap |
Integer | nil |
Maximum frames per second |
render_mode |
Symbol | :continuous |
:continuous or :on_demand (see On-demand rendering) |
resizable |
Boolean | false |
Whether the window can be resized |
highdpi |
Boolean | true |
Enable high-DPI rendering |
pixel_scale |
Boolean | false |
Scale rendering to match pixel density |
viewport_width |
Integer | Same as width |
Drawable area width |
viewport_height |
Integer | Same as height |
Drawable area height |
viewport |
Symbol | :letterbox |
Viewport scaling mode for resizable windows |
cursor |
Symbol | :visible |
:visible, :hidden, or a system cursor symbol (see Cursor Control) |
show_fps |
Boolean | false |
Display an FPS counter |
diagnostics |
Boolean | false |
Print diagnostic messages |
close_on_esc |
Boolean | false |
Close the window when Escape is pressed |
Here’s a more complete example using several attributes together:
set title: 'My App'
set width: 800, height: 600
set background: 'navy'
set fps_cap: 60
set icon: 'icon.png'
set resizable: true
set highdpi: true
set show_fps: true
set close_on_esc: true
Pixel scale
On high-DPI screens (like Retina displays), the operating system scales things so a 640×480 window looks the same physical size as it would on a standard display. This is great most of the time, but if you want access to every individual pixel on screen, you can enable pixel scaling:
set pixel_scale: true
With this enabled, your drawing area grows to match the actual pixel density of the display. On a 2× Retina screen, a 640×480 window gives you a 1280×960 drawing area — twice the detail!
You’ll still use width and height for setting the window size. To get the actual drawing area in pixels, use viewport_width and viewport_height. These values are available once the window is shown, so you’ll want to use them inside the update loop:
set width: 640, height: 480, pixel_scale: true
canvas = nil
update do
next if canvas
canvas = Canvas.new(width: Window.viewport_width, height: Window.viewport_height)
end
show
If you want to make a full-screen app with pixel scaling, use display_width and display_height to set the window size:
set width: Window.display_width, height: Window.display_height, pixel_scale: true
Viewport scaling
When a window can be resized (or runs on a high-DPI display, or uses pixel scaling), the drawing area and the window are no longer necessarily the same size. The viewport attribute decides how your content gets mapped onto whatever space is available:
set viewport: :integer
| Mode | What it does |
|---|---|
:letterbox |
(default) Scales to fit while keeping the aspect ratio; any leftover space shows as bars. |
:stretch |
Scales to fill the window, ignoring the aspect ratio — content can distort. |
:integer |
Scales by whole-number multiples only, so pixel art stays crisp; may leave a border. |
:overscan |
Scales to fill the window while keeping the aspect ratio, cropping whatever spills over the edges. |
:expand |
Doesn’t scale a fixed canvas — grows the drawing area to match the window, so more of your scene becomes visible as it gets bigger. |
:fixed |
No scaling at all; draws the viewport at 1:1 and centers it in the window. |
Reach for :integer when you’re making a pixel-art game and want every pixel to stay sharp, and :expand when you’d rather show more of the world than zoom into it.
Getting attributes
Sometimes it’s also helpful to get the value of a window attribute, so there’s a method for that too called get:
get :width # returns `640`, for example
For every attribute you can set, you can also get its value by providing the Symbol. You can only get one attribute at a time. Here are some extra read-only attributes you can get:
| Attribute | Description |
|---|---|
:window |
The window object itself, just in case you want to inspect it |
:frames |
The number of frames that have been rendered since the start |
:fps |
The current frame rate expressed in frames per second |
:fps_cap |
The current FPS cap, if one has been set |
:mouse_x |
The x-coordinate position of the mouse, relative to the window |
:mouse_y |
The y-coordinate position of the mouse, relative to the window |
:display_width |
The width of the display (screen) in logical pixels |
:display_height |
The height of the display (screen) in logical pixels |
:display_pixel_width |
The width of the display in physical pixels (differs from :display_width on HiDPI screens) |
:display_pixel_height |
The height of the display in physical pixels (differs from :display_height on HiDPI screens) |
The Window class
When you require 'ruby2d', a new window is instantiated for you by calling Ruby2D::Window.new. Sometimes it might be convenient to reference the Window class directly, for example when retrieving attributes:
Window.title # returns "Ruby 2D"
Window.width # returns 640
Window.height # returns 480
Window.fps # current FPS
Window.mouse_x # mouse x position
Window.mouse_y # mouse y position
Window.display_width # display size in logical pixels
Window.display_height
Window.display_pixel_width # display size in physical pixels
Window.display_pixel_height
Screenshots
Want to capture what’s on screen? You can save a screenshot at any time:
screenshot './my_screenshot.png' # save to a specific path
screenshot # auto-generated timestamped filename
Window.screenshot './my_screenshot.png' # also available on the Window class
The image gets written at the end of the frame you asked for it in, so screenshot hands the path back a moment before the file actually lands on disk. By the time your next update runs, it’s there.
Asking for a screenshot also forces that frame to draw, so a capture in on-demand rendering will never hand you a stale, parked frame. That’s also why this saves the image even though the window closes in the same tick:
update do
screenshot './my_screenshot.png'
close
end
One thing to know: taking a screenshot after the window has closed raises an error, since there’s no frame left to write.
Cursor control
You can show, hide, or change the mouse cursor with a single setter:
set cursor: :hidden # hide the cursor
set cursor: :visible # show the default cursor
# Or directly:
Window.cursor = :hidden
Window.cursor = :visible
Window.cursor # => :default, :hidden, :pointer, etc.
:visible is a set-only convenience meaning “show the default arrow.” When you read Window.cursor back, a plain visible cursor reports as :default (or a system-cursor name), never :visible.
System cursors
The same setter takes any of the system-provided cursor shapes:
set cursor: :pointer
set cursor: :crosshair
# ...and more, see table below
# Or directly:
Window.cursor = :pointer
| Value | Description |
|---|---|
:default |
Default arrow pointer |
:pointer |
Pointing hand (links, buttons) |
:text |
I-beam (text fields) |
:crosshair |
Crosshair / precision pointer |
:move |
Four-way move arrow |
:wait |
Busy / loading |
:progress |
Busy with arrow |
:not_allowed |
Slashed circle |
:ew_resize |
Horizontal resize (left–right) |
:ns_resize |
Vertical resize (up–down) |
:nwse_resize |
Diagonal resize (NW–SE) |
:nesw_resize |
Diagonal resize (NE–SW) |
:n_resize |
Edge resize: north |
:ne_resize |
Edge resize: north-east |
:e_resize |
Edge resize: east |
:se_resize |
Edge resize: south-east |
:s_resize |
Edge resize: south |
:sw_resize |
Edge resize: south-west |
:w_resize |
Edge resize: west |
:nw_resize |
Edge resize: north-west |
The Window class pattern
For more structured applications, you can subclass Ruby2D::Window directly. This gives you a self-contained game or app class with polling-based input methods (more on those in the input section):
require 'ruby2d/core'
class Game < Ruby2D::Window
def initialize
super(title: 'My Game', width: 800, height: 600)
@player = Square.new(x: 100, y: 100, size: 50, color: 'blue')
end
def update
@player.x += 1 if key_held? :right
@player.x -= 1 if key_held? :left
end
end
Game.new.show
Note we require 'ruby2d/core' instead of require 'ruby2d': this loads the classes without the DSL mixin, so everything stays neatly inside your class.
The update loop
The window also manages the update loop, one of the few infinite loops in programming you’ll encounter that isn’t a mistake. Every window has a heartbeat, a loop that runs as fast as your display refreshes (or up to your fps_cap). Using the update method, we can enter this loop and make the window come to life!
Say we’re bored with the static background we currently have. Let’s try changing it to a random color periodically:
require 'ruby2d'
tick = 0
update do
if tick % 60 == 0
set background: 'random'
end
tick += 1
end
show
How does this work? First, we set a variable called tick to 0. Then, we enter the update loop and do something interesting, like dividing tick by 60 and checking if its remainder equals 0. Each cycle of the loop, we increment tick by one. When the remainder does equal 0, we set the background color to 'random'.
Moving things at a steady speed
There’s a subtle catch once you start animating with the loop: it runs once per frame, and not every display refreshes at the same rate. If you nudge a shape along by a fixed amount each frame:
update do
@box.x += 1 # one pixel per frame
end
It travels at 60 pixels per second on a 60Hz screen, but 120 pixels per second on a 120Hz one. Same code, twice the speed.
The fix is to think in seconds rather than frames. The update block can take a single argument (call it dt, for “delta time”), which is the number of seconds that passed since the last update. Multiply your motion by it and everything moves at the same real-world speed, whatever the refresh rate:
update do |dt|
@box.x += 60 * dt # always 60 pixels per second, on any display
end
In the Window class pattern, the same value is available as delta_time:
class Game < Ruby2D::Window
def update
@box.x += 60 * delta_time
end
end
A couple of details worth knowing: dt is 0.0 on the very first frame (nothing has elapsed yet), and it’s capped at 0.1 seconds, so if the window stalls or you drag it around, your animation won’t lurch forward by a huge jump when things pick back up.
Absolute time with elapsed
dt tells you how long the last frame took, which is just what you want for motion. But sometimes you care about total time instead: how long since the program started, whether a cooldown has finished, when to fire the next thing. For that, reach for elapsed.
elapsed is the number of seconds since your program started. It starts near 0 and only ever counts upward, which makes it perfect for “has enough time passed?” checks, especially inside event handlers, which don’t get a dt of their own:
# Fire at most once every half second, even on a flurry of clicks
on :mouse_down do
next unless elapsed >= (@next_shot || 0)
shoot
@next_shot = elapsed + 0.5
end
You might reach for Time.now here out of habit, but resist it. On the web and in native builds your code runs on mruby, where Time.now is a coarse wall clock (its millisecond values actually overflow in the browser) and Process.clock_gettime doesn’t exist at all. dt and elapsed sidestep all of that and read the same everywhere. Save Time.now for real-world dates and times.
The render block
In addition to update, there’s also a render block that runs every frame after update. This is useful for one-off drawing with the class-level .render methods (shapes you want to draw each frame without creating persistent objects):
render do
Rectangle.render(x: 0, y: 0, width: 50, height: 50, color: 'red')
end
Objects created with .new are rendered automatically. The render block is only needed when you want to do custom per-frame drawing.
Placing the block in the z-order
By default, whatever the render block draws lands on top of everything, in front of every object you created with .new. That’s usually just right: the block is where the live, per-frame action happens, so you want to see it.
But not always. Imagine a game with a world that scrolls past under the action and a score panel that stays put in the corner. If the per-frame drawing sweeps over that panel, your score vanishes behind it. What you really want is three layers: a backdrop underneath, the moving world in the middle, and the panel on top of both.
Pass z: to slot the block anywhere in the z-order, right alongside your .new objects:
render do … end # :foreground (default), on top of everything
render z: :background do … end # behind everything
render z: 10 do … end # slotted in at depth 10, the same scale as an object's z
With a number, the rule is simple: objects at that z or lower draw first, then the block, then everything above. (:foreground and :background are just the two far ends of that scale.) So the three-layer scene falls out naturally:
Quad.new(...) # backdrop at a low z, drawn first
render z: 10 do ... end # the per-frame world, in the middle
Text.new(..., z: 20) # the panel on top, safely above the action
One rule of thumb before you reach for it: only set z: when the block’s drawing actually overlaps a persistent object. If your panel sits in its own reserved strip, well clear of the drawing, the default is already correct and adding z: is just noise. It earns its place when the drawing roams over a persistent object, or when you have persistent layers on both sides of the block.
On-demand rendering
Normally, Ruby 2D draws a fresh frame every tick of the loop, perfect for games and animations, where something new is happening all the time. But what if your app only changes when the user does something? A chart that updates on a click, a dashboard that refreshes every now and then, a text editor waiting patiently for you to type. Drawing 60 identical frames a second for an app that’s just sitting there is a lot of work for your GPU to do for nothing!
For apps like these, Ruby 2D has a second mode called on-demand rendering. In this mode, the window only draws a new frame when you ask it to:
require 'ruby2d'
set title: 'My App', render_mode: :on_demand
show
The two possible values are :continuous (the default, drawing every tick) and :on_demand (drawing only when asked).
Asking for a frame
Once you’re in on-demand mode, the window will happily sit there doing nothing until you give it a nudge. That nudge is called request_render. Call it whenever you change something on screen:
on :mouse_down do |e|
@selected = hit_test(e.x, e.y)
request_render
end
on :key_down do |e|
case e.key
when 'up' then scroll(-10); request_render
when 'down' then scroll( 10); request_render
end
end
Call it as many times as you like. If you call it ten times in one tick, you’ll still only get one frame. It’s also safe to call from any thread, so don’t worry about where it happens.
Ruby 2D will also draw a frame on its own in a few situations where you’d expect one: the very first frame when the window opens, when the operating system asks for a redraw (like after resizing or moving the window to another display), and when you take a screenshot. Everything else is up to you.
What still happens every tick
On-demand mode only skips the drawing work. Everything else in the loop keeps ticking along as usual, so your app stays nice and responsive:
- Events still fire (
on :key_down,on :mouse_move, and friends) - The
updateblock still runs - Mouse position is still tracked (
Window.mouse_x,Window.mouse_y) - Frame pacing still respects your
fps_cap
And Window.frames still counts ticks of the loop (not drawn frames), so it behaves exactly the same in both modes.
Animations in on-demand mode
What if you want something that animates on its own, like a blinking cursor or a little spinner? No problem, just call request_render whenever it’s time for the animation to move forward. Here’s a cursor that blinks every half second:
update do
if elapsed - (@last_blink || 0) >= 0.5
@caret_visible = !@caret_visible
@last_blink = elapsed
request_render
end
end
This is by design — you’re in charge of what’s worth a frame.
Switching modes on the fly
You can swap between modes whenever you like, even after show has been called. A fresh frame is guaranteed on the next tick after switching, so you’ll never be left looking at a stale window.
A few things to know
- The FPS counter (
show_fps: true) anddiagnosticsstill work in on-demand mode, but they’ll only update when a frame is actually drawn, which is usually exactly what you want. on :key_heldfires every tick, so rendering in response to it defeats the whole point. Use:key_downand:key_upwith your own state instead.- If you’re making a game, stick with the default
:continuousmode. On-demand is for apps that spend most of their time waiting.
Closing the window
When you’re done with a window, there’s nothing left to do but close it. You probably closed windows in the examples above by clicking the close button on the title bar, or using a keyboard shortcut. But what if you want to close the window with code? There’s a method for that!
The trick is that close needs to be called from within the loop, since after show is called, the loop takes over. Here’s an example that closes the window after 5 seconds:
require 'ruby2d'
update do
# Close the window after 5 seconds
close if elapsed > 5
end
show
Or, if you just want Escape to close the window, there’s a shortcut for that:
set close_on_esc: true
Close event
The :close event fires just before the window closes, whether the user clicks the OS close button or close is called programmatically. Use it to run any cleanup logic on the way out:
on :close do
puts 'Goodbye!'
end
Only one :close handler can be registered at a time. Registering a new one replaces the previous.
close and on(:close) work together. Calling close also fires the :close handler before shutting down:
update do
close if get(:frames) > 300
end
on :close do
puts 'Closing after 300 frames'
end
On the web
A page can’t close itself, only the person viewing it can. So on the web, close does nothing: the :close handler doesn’t run, and your app keeps going. Anything you’d put after a close, like a farewell screen or a final score, wants to be drawn rather than waited for. A quit the viewer initiates still fires on(:close).
That’s it!
You’ve learned all there is to know about the window in Ruby 2D. Continue to the next topic