Let me start by giving a small disclaimer. I’m fully aware that the UI system I’m implementing here is far from being a nice one. The UI system I’ll be presenting over the several next posts should be good enough to get this project to it’s first milestone. Once that is done, we’ll probably implement a better one. But for now…
The docs tell us, that in order to render any text, we need to begin by having a font. I’ll fetch titilium web, put it into ./assets/fonts/titilium
, and update the constants like so:
# src/constants.cr
module Constants
FONT = SF::Font.from_file("./assets/fonts/titilium/TitilliumWeb-Regular.ttf")
COLOR::MENU::BACKGROUND = SF::Color.new(96, 96, 96)
COLOR::MENU::BACKGROUND_BORDER = SF::Color.new(227, 227, 227)
end
And we have a font now. Let’s move on to…
So what is a button? That’s easy, button is something that:
A simple, initial, implementation could look like this:
# src/lib/ui/button.cr
module UI
class Button
@bounding_rectangle : SF::IntRect
@text : String
@on_click : Proc(Nil)
def initialize(position, size, text, on_click)
# we combine position and size to produce @bounding_rectangle
@bounding_rectangle = SF::IntRect.new(position[0], position[1], size[0], size[1])
# we save the buttons text and action
@text = text
@on_click = on_click
end
def draw(target : SF::RenderTarget, states : SF::RenderStates)
# Define a grey rectangle to be rendered at @bounding_rectangle
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = SF::Color.new(142, 142, 142)
# Define a text object. We needed to have a font object first, because it's needed here
text = SF::Text.new(@text, Constants::FONT, 60)
text.color = SF::Color::Black
text.position = @bounding_rectangle.position
# Render both
target.draw(background, states)
target.draw(text, states)
end
end
end
We’re going to make a good use of it in the bottom menu:
# 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])
end
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = Constants::COLOR::MENU::BACKGROUND
button = UI::Button.new(
position: @bounding_rectangle.position,
size: {200, 80},
text: "btn",
on_click: -> { p "Click!" }
)
target.draw(background, states)
target.draw(button, states)
end
end
end
And there we have it:
That’s a button, allright. Mission accomplished. It needs several improvements, though:
The first part we can cheat for now. The bottom menu has 120px of height, and the button has 80px of height. If we render it 20px to the right and to the bottom, from it’s current position, that should be just fine. I’ll just update the button definition to this:
# src/the_empire/bottom_menu.cr, class TheEmpire::BottomMenu#draw
button = UI::Button.new(
position: @bounding_rectangle.position.map {|v| v + 20}, # update both position values to be 20 larger
size: {200, 80},
text: "btn",
on_click: -> { p "Click!" }
)
Which gives us this:
Nice and centered. We have successfully avoided the necessity to think (about that specific problem, for now).
That part, we actually need to think through a little bit. We’ll be adding a lot of different buttons, and they will be allowed to have different sizes, font sizes, and have different texts. For all of them, the text should be both vertically and horizontally centered. Since text is basically a rectangle, center one within the bounding rectangle should totally, really be fairly simple.
If we take the background
left
position, and add half of the background
width, we should get the middle of the buttons x
position. If we then substract half of the text
width, that should give us the expected x
position. Analogous solution for height should workas well.
That’s easy math, 20 minutes adventure, in and out:
# src/lib/ui/button.cr, class UI::Button
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = SF::Color.new(142, 142, 142)
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_position_y = @bounding_rectangle.top + @bounding_rectangle.height / 2 - text.local_bounds.height / 2
text.position = { text_position_x, text_position_y }
target.draw(background, states)
target.draw(text, states)
end
And that should do just fine! Let’s see:
This is not centered at all.
Like I mentioned in the beginning, I am re-creating my steps from a while back with these posts, and that specific issue gives me painful flashbacks. It’s a very interesting experience, rediscovering bugs that I already fixed weeks ago. Ones I was hoping never to see again. But well, here we are: We are asking the text to be rendered within a very specific bounding rectangle, and it is instead being rendered quite a good bit off.
I have spent good, long, painful hours trying to understand that bit, but it turns out to be the expected behavior. Managing text on a screen is really difficult, and people way smarter than me decided that’s a good default behavior. I have later learned that those people were right, but we’ll get there in due time.
Long story short, to do what we want, we need to not only use texts width
and height
, but also top
and left
. Texts bounds are nice enough to tell us exactly how much they are off:
# src/lib/ui/button.cr, class UI::Button
def draw(target : SF::RenderTarget, states : SF::RenderStates)
background = SF::RectangleShape.new(@bounding_rectangle)
background.fill_color = SF::Color.new(142, 142, 142)
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).to_i
text_position_y = (@bounding_rectangle.top + @bounding_rectangle.height / 2 - text.local_bounds.height / 2 - text.local_bounds.top).to_i
text.position = { text_position_x, text_position_y }
target.draw(background, states)
target.draw(text, states)
end
Figuring this one out was especially tricky since it’s not explained at all in the CrSGML Text doc, and I only learned what’s happening, and why it is happening after reading a random forum thread.
Anyway, that works like expected:
And that takes us to the really tricky part. Making that button react to clicks will require us to think a little bit about events management!
Allons-y!