Let’s go through everything we have in place for events management, as provided by the CrSFML library.
We have a main loop, an indefinitely-running piece of code, which runs in loop:
# src/main.cr
require "./the_empire"
the_empire = TheEmpire.new
while the_empire.running?
the_empire.handle_events
the_empire.update
the_empire.render
end
From this, the part we are interested in is the_empire.handle_events
. In every loop we process, we will attempt to process the events:
# src/the_empire.cr, class TheEmpire
def handle_events
while event = @window.poll_event
handle_event(event)
@world_map.handle_event(event)
end
end
We are calling @window.poll_event
, for as long as long it returns values. For each event we get, we call handle_event(event)
and @world_map.handle_event(event)
.
The first of these is defined like this:
# src/the_empire.cr, class TheEmpire
def handle_event(event : SF::Event::Closed)
@window.close
end
# Ignore any other event
def handle_event(event)
end
So its only job is to close the window if SF::Event::Closed
event comes along. @world_map.handle_event(event)
uses all remaining events to move the map.
Wait. Like… All of them?
Oh boy, yeah, so that’s a problem.
What is happening here is that we are clicking and moving mouse around in right menu and bottom menus, but those clicks and moves are being applied to the world map.
That’s not the behavior we are expecting. What we want is this: If I press on WorldMap
, WorldMap
should react, and if I press on BottomMenu
, BottomMenu
should react.
Those shouldn’t mix.
I’ll update the events handling code to look like this:
# src/the_empire.cr, class TheEmpire
def handle_events
while event = @window.poll_event
handle_event(event)
end
end
# Handle the close event specifically
def handle_event(event : SF::Event::Closed)
@window.close
end
def handle_event(event)
@world_map.handle_event(event)
@bottom_menu.handle_event(event)
@right_menu.handle_event(event)
end
#handle_events
now picks the event from @window
and passess them along. #handle_event
closes the window, if that event is SF::Event::Closed
, but otherwise delegates the event to all three of @world_map
, @bottom_menu
and @right_menu
. Those classess will now accept all events and will need to decide whether or not they should react to them.
BottomMenu
and RightMenu
will get an empty method for now:
def handle_event(event)
end
This is how WorldMap
handles mouse events currently:
# src/the_empire/world_map.cr, class TheEmpire::WorldMap
def handle_event(event : SF::Event::MouseButtonPressed)
@moving_around = true
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
def handle_event(event : SF::Event::MouseButtonReleased)
@moving_around = false
end
def handle_event(event : SF::Event::MouseMoved)
if @moving_around
x_delta = @mouse_button_initial_x - event.x
y_delta = @mouse_button_initial_y - event.y
move({x_delta, y_delta})
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
end
We want to make sure that this processing only happens if mouse is over the WorldMap
. We might do it like like this:
# src/the_empire/world_map.cr, class TheEmpire::WorldMap
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@moving_around = true
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
end
def handle_event(event : SF::Event::MouseButtonReleased)
if @bounding_rectangle.contains?(event.x, event.y)
@moving_around = false
end
end
def handle_event(event : SF::Event::MouseMoved)
if @bounding_rectangle.contains?(event.x, event.y)
if @moving_around
x_delta = @mouse_button_initial_x - event.x
y_delta = @mouse_button_initial_y - event.y
move({x_delta, y_delta})
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
end
end
@bounding_rectangle
is, of course, a rectangle, and @bounding_rectangle.contains?(event.x, event.y)
will tell is whether or not the events point is contained by it. Neat.
Does it work? It does!
Does it, though?
If I press the mouse, move outside of the WorldMap
, release the mouse and move back to the WorldMap
, we have ended in a state where the mouse is not pressed, but map is being dragged behind the mouse.
What I would really like, is to have MouseEnter
and MouseLeave
events for each container, but we don’t have that. So what I’ll do, is this:
# src/the_empire/world_map.cr, class TheEmpire::WorldMap
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@moving_around = true
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
end
def handle_event(event : SF::Event::MouseButtonReleased)
@moving_around = false
end
def handle_event(event : SF::Event::MouseMoved)
if @moving_around
x_delta = @mouse_button_initial_x - event.x
y_delta = @mouse_button_initial_y - event.y
move({x_delta, y_delta})
@mouse_button_initial_x = event.x
@mouse_button_initial_y = event.y
end
end
Begin drag-and-drop if mouse is over the WorldMap
, but process and finish it irregardless of position.
That will work for now. There is still handling of AWSD
keys and mouse scroll. Those will ignore the position boundary, but that’s an issue for another day.
And that leaves us with the final missing piece: handling the button. We’ll update the BottomMenu
like so:
# src/the_empire/bottom_menu.cr
class TheEmpire
class BottomMenu
include SF::Drawable
def initialize(position, size)
@bounding_rectangle = SF::IntRect.new(position[0], position[1], size[0], size[1])
@button = UI::Button.new(
position: @bounding_rectangle.position.map {|v| v + 20},
size: {200, 80},
text: "btn",
on_click: -> { p "Click!" }
)
end
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = Constants::COLOR::MENU::BACKGROUND
target.draw(background, states)
target.draw(@button, states)
end
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@button.handle_event(event)
end
end
def handle_event(event)
end
end
end
We moved the button definition to initializer and we’re caching it in an instance variable. In the #draw
, we’re rendering the instance variable, and we’re defining a #handle_event
method, which reacts to a SF::Event::MouseButtonPressed
event. It then checks if the event is inside @bounding_rectangle
, and if it is, we are passing the event to the @button
. And the final piece:
# src/lib/ui/button.cr class UI::Button
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@on_click.call
end
end
We are defining a #handle_event
on Button
. If it contains the event, we call the @on_click
function. That works just fine. But since our confirmation for working is something printed to the console, it’s rather anticlimactic.
I have a better idea:
# src/the_empire/bottom_menu.cr
class TheEmpire
class BottomMenu
include SF::Drawable
def initialize(position, size)
@bounding_rectangle = SF::IntRect.new(position[0], position[1], size[0], size[1])
@button1 = UI::Button.new(
position: { @bounding_rectangle.position[0] + 20, @bounding_rectangle.position[1] + 20},
size: {200, 80},
text: "btn1",
on_click: ->(button : UI::Button) {
deactivate_all!
activate(button)
}
)
@button2 = UI::Button.new(
position: { @bounding_rectangle.position[0] + 240, @bounding_rectangle.position[1] + 20},
size: {200, 80},
text: "btn2",
on_click: ->(button : UI::Button) {
deactivate_all!
activate(button)
}
)
end
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = Constants::COLOR::MENU::BACKGROUND
target.draw(background, states)
target.draw(@button1, states)
target.draw(@button2, states)
end
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@button1.handle_event(event)
@button2.handle_event(event)
end
end
def handle_event(event)
end
private def deactivate_all!
@button1.active = false
@button2.active = false
end
private def activate(button : UI::Button)
button.active = true
end
end
end
We updated the BottomMenu
to have 2 buttons. Let’s zoom in on a tricky part of this. We pass this to both buttons as action:
on_click: ->(button : UI::Button) {
deactivate_all!
activate(button)
}
What I really want to achieve is this: If a button is clicked, set it as active, and set the other one as inactive. My initial idea for implementation was this:
on_click: -> {
@button1.active = true
@button2.active = false
}
But that doesn’t compile, and we get the following error:
Instance variable '@button1' was used before it was initialized in one of the 'initialize' methods, rendering it nilable
While initializing @button1
, we are trying to create a Proc
, that depends on @button1
, and compiler doesn’t like that.
The implementation I ended up with isn’t exactly elegant, but it gets the job done. It also required a small update to the UI::Button
:
# src/lib/ui/button.cr
module UI
class Button
include SF::Drawable
@bounding_rectangle : SF::IntRect
@text : String
@on_click : Proc(UI::Button, Bool)
property active : Bool = false
def initialize(position, size, text, on_click)
@bounding_rectangle = SF::IntRect.new(position[0], position[1], size[0], size[1])
@text = text
@on_click = on_click
end
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
if self.active
background.fill_color = SF::Color.new(249, 180, 75)
else
background.fill_color = SF::Color.new(142, 142, 142)
end
text = SF::Text.new(@text, Constants::FONT, 60)
text.color = SF::Color::Black
text_position_x = @bounding_rectangle.left + @bounding_rectangle.width / 2 - text.local_bounds.width / 2 - text.local_bounds.left
text_position_y = @bounding_rectangle.top + @bounding_rectangle.height / 2 - text.local_bounds.height / 2 - text.local_bounds.top
text.position = { text_position_x, text_position_y }
target.draw(background, states)
target.draw(text, states)
end
def handle_event(event : SF::Event::MouseButtonPressed)
if @bounding_rectangle.contains?(event.x, event.y)
@on_click.call(self)
end
end
end
end
The button will now call @on_click
with self
, and there is an #active
boolean property, which changes the background color.
And that should produce a sufficient spectacle:
And that’s it. In the next post, we will be introducing our first modes of operation!