New example: text/unicode
This commit is contained in:
parent
5100377cde
commit
52946ea71d
8 changed files with 2580 additions and 0 deletions
396
examples/text/unicode/main.go
Normal file
396
examples/text/unicode/main.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
/*******************************************************************************************
|
||||
*
|
||||
* raylib [text] example - Unicode
|
||||
*
|
||||
* Example originally created with raylib 2.5, last time updated with raylib 4.0
|
||||
*
|
||||
* Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5)
|
||||
*
|
||||
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
|
||||
* BSD-like license that allows static linking with closed source software
|
||||
*
|
||||
* Copyright (c) 2019-2024 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5)
|
||||
*
|
||||
********************************************************************************************/
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
const (
|
||||
screenWidth = 800
|
||||
screenHeight = 450
|
||||
emojiPerWidth = 8
|
||||
emojiPerHeight = 4
|
||||
MeasureState = 0
|
||||
DrawState = 1
|
||||
)
|
||||
|
||||
type Messages struct {
|
||||
text string
|
||||
language string
|
||||
}
|
||||
|
||||
type Emoji struct {
|
||||
index int32 // Index inside `emojiCodepoints`
|
||||
message int32 // Message index
|
||||
color rl.Color // Emoji color
|
||||
}
|
||||
|
||||
// Arrays that holds the random emojis
|
||||
var emoji [emojiPerWidth * emojiPerHeight]Emoji
|
||||
var hovered, selected int32 = -1, -1
|
||||
|
||||
func main() {
|
||||
rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagVsyncHint)
|
||||
rl.InitWindow(screenWidth, screenHeight, "raylib [text] example - unicode")
|
||||
|
||||
// Load the font resources
|
||||
// NOTE: fontAsian is for asian languages,
|
||||
// fontEmoji is the emojis and fontDefault is used for everything else
|
||||
fontDefault := rl.LoadFont("resources/dejavu.fnt")
|
||||
fontAsian := rl.LoadFont("resources/noto_cjk.fnt")
|
||||
fontEmoji := rl.LoadFont("resources/symbola.fnt")
|
||||
|
||||
hoveredPos := rl.Vector2{}
|
||||
selectedPos := rl.Vector2{}
|
||||
|
||||
RandomizeEmoji()
|
||||
|
||||
rl.SetTargetFPS(60) // Set our game to run at 60 frames-per-second
|
||||
|
||||
// Main loop
|
||||
for !rl.WindowShouldClose() { // Detect window close button or ESC key
|
||||
// Update
|
||||
|
||||
// Add a new set of emojis when SPACE is pressed
|
||||
if rl.IsKeyPressed(rl.KeySpace) {
|
||||
RandomizeEmoji()
|
||||
}
|
||||
|
||||
// Set the selected emoji
|
||||
if rl.IsMouseButtonPressed(rl.MouseButtonLeft) && (hovered != -1) && (hovered != selected) {
|
||||
selected = hovered
|
||||
selectedPos = hoveredPos
|
||||
}
|
||||
|
||||
mouse := rl.GetMousePosition()
|
||||
position := rl.Vector2{
|
||||
X: 28.8,
|
||||
Y: 10.0,
|
||||
}
|
||||
hovered = -1
|
||||
|
||||
// Draw
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.RayWhite)
|
||||
|
||||
// Draw random emojis in the background
|
||||
for i := int32(0); i < emojiPerWidth*emojiPerHeight; i++ {
|
||||
txt := emojiCodepoints[emoji[i].index : emoji[i].index+4]
|
||||
emojiRect := rl.Rectangle{
|
||||
X: position.X,
|
||||
Y: position.Y,
|
||||
Width: float32(fontEmoji.BaseSize),
|
||||
Height: float32(fontEmoji.BaseSize),
|
||||
}
|
||||
|
||||
if !rl.CheckCollisionPointRec(mouse, emojiRect) {
|
||||
col := rl.Fade(rl.LightGray, 0.4)
|
||||
if selected == i {
|
||||
col = emoji[i].color
|
||||
}
|
||||
rl.DrawTextEx(fontEmoji, txt, position, float32(fontEmoji.BaseSize), 1.0, col)
|
||||
} else {
|
||||
rl.DrawTextEx(fontEmoji, txt, position, float32(fontEmoji.BaseSize), 1.0, emoji[i].color)
|
||||
hovered = i
|
||||
hoveredPos = position
|
||||
}
|
||||
|
||||
if (i != 0) && (i%emojiPerWidth == 0) {
|
||||
position.Y += float32(fontEmoji.BaseSize) + 24.25
|
||||
position.X = 28.8
|
||||
} else {
|
||||
position.X += float32(fontEmoji.BaseSize) + 28.8
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the message when an emoji is selected
|
||||
if selected != -1 {
|
||||
message := emoji[selected].message
|
||||
horizontalPadding := 20
|
||||
verticalPadding := 30
|
||||
font := fontDefault
|
||||
|
||||
// Set correct font for asian languages
|
||||
if messages[message].language == "Chinese" ||
|
||||
messages[message].language == "Korean" ||
|
||||
messages[message].language == "Japanese" {
|
||||
font = fontAsian
|
||||
}
|
||||
|
||||
// Calculate size for the message box (approximate the height and width)
|
||||
sz := rl.MeasureTextEx(font, messages[message].text, float32(font.BaseSize), 1.0)
|
||||
if sz.X > 300 {
|
||||
sz.Y *= sz.X / 300
|
||||
sz.X = 300
|
||||
} else if sz.X < 160 {
|
||||
sz.X = 160
|
||||
}
|
||||
|
||||
msgRect := rl.Rectangle{
|
||||
X: selectedPos.X - 38.8,
|
||||
Y: selectedPos.Y,
|
||||
Width: float32(2*horizontalPadding) + sz.X,
|
||||
Height: float32(2*verticalPadding) + sz.Y,
|
||||
}
|
||||
msgRect.Y -= msgRect.Height
|
||||
|
||||
// Coordinates for the chat bubble triangle
|
||||
a := rl.Vector2{
|
||||
X: selectedPos.X,
|
||||
Y: msgRect.Y + msgRect.Height,
|
||||
}
|
||||
b := rl.Vector2{
|
||||
X: a.X + 8,
|
||||
Y: a.Y + 10,
|
||||
}
|
||||
c := rl.Vector2{
|
||||
X: a.X + 10,
|
||||
Y: a.Y,
|
||||
}
|
||||
|
||||
// Don't go outside the screen
|
||||
if msgRect.X < 10 {
|
||||
msgRect.X += 28
|
||||
}
|
||||
if msgRect.Y < 10 {
|
||||
msgRect.Y = selectedPos.Y + 84
|
||||
a.Y = msgRect.Y
|
||||
c.Y = a.Y
|
||||
b.Y = a.Y - 10
|
||||
|
||||
// Swap values so we can actually render the triangle :(
|
||||
a, b = b, a
|
||||
}
|
||||
|
||||
if msgRect.X+msgRect.Width > screenWidth {
|
||||
msgRect.X -= (msgRect.X + msgRect.Width) - screenWidth + 10
|
||||
}
|
||||
|
||||
// Draw chat bubble
|
||||
rl.DrawRectangleRec(msgRect, emoji[selected].color)
|
||||
rl.DrawTriangle(a, b, c, emoji[selected].color)
|
||||
|
||||
// Draw the main text message
|
||||
textRect := rl.Rectangle{
|
||||
X: msgRect.X + float32(horizontalPadding)/2,
|
||||
Y: msgRect.Y + float32(verticalPadding)/2,
|
||||
Width: msgRect.Width - float32(horizontalPadding),
|
||||
Height: msgRect.Height,
|
||||
}
|
||||
DrawTextBoxed(font, messages[message].text, textRect, float32(font.BaseSize), 1.0, true, rl.White)
|
||||
|
||||
// Draw the info text below the main message
|
||||
size := len(messages[message].text)
|
||||
length := utf8.RuneCountInString(messages[message].text)
|
||||
info := fmt.Sprintf("%s %d characters %d bytes", messages[message].language, length, size)
|
||||
sz = rl.MeasureTextEx(rl.GetFontDefault(), info, 10, 1.0)
|
||||
|
||||
rl.DrawText(info, int32(textRect.X+textRect.Width-sz.X), int32(msgRect.Y+msgRect.Height-sz.Y-2), 10,
|
||||
rl.RayWhite)
|
||||
|
||||
}
|
||||
|
||||
// Draw the info text
|
||||
rl.DrawText("These emojis have something to tell you, click each to find out!", (screenWidth-650)/2,
|
||||
screenHeight-40, 20, rl.Gray)
|
||||
rl.DrawText("Each emoji is a unicode character from a font, not a texture... Press [SPACEBAR] to refresh",
|
||||
(screenWidth-484)/2, screenHeight-16, 10, rl.Gray)
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
// De-Initialization
|
||||
rl.UnloadFont(fontDefault) // Unload font resource
|
||||
rl.UnloadFont(fontAsian) // Unload font resource
|
||||
rl.UnloadFont(fontEmoji) // Unload font resource
|
||||
|
||||
rl.CloseWindow() // Close window and OpenGL context
|
||||
}
|
||||
|
||||
// RandomizeEmoji fills the emoji array with random emoji (only those emojis present in fontEmoji)
|
||||
func RandomizeEmoji() {
|
||||
hovered, selected = -1, -1
|
||||
start := rl.GetRandomValue(45, 360)
|
||||
|
||||
for i := int32(0); i < emojiPerWidth*emojiPerHeight; i++ {
|
||||
// 0-179 emoji codepoints (from emoji char array) each 4bytes + null char
|
||||
emoji[i].index = rl.GetRandomValue(0, 179) * 5
|
||||
|
||||
// Generate a random color for this emoji
|
||||
emoji[i].color = rl.Fade(rl.ColorFromHSV(float32((start*(i+1))%360), 0.6, 0.85), 0.8)
|
||||
|
||||
// Set a random message for this emoji
|
||||
emoji[i].message = rl.GetRandomValue(0, int32(len(messages)-1))
|
||||
}
|
||||
}
|
||||
|
||||
// DrawTextBoxed draws text using font inside rectangle limits
|
||||
func DrawTextBoxed(font rl.Font, text string, rec rl.Rectangle, fontSize, spacing float32, wordWrap bool, tint rl.Color) {
|
||||
DrawTextBoxedSelectable(font, text, rec, fontSize, spacing, wordWrap, tint, 0, 0, rl.White, rl.White)
|
||||
}
|
||||
|
||||
// DrawTextBoxedSelectable draws text using font inside rectangle limits with support for text selection
|
||||
func DrawTextBoxedSelectable(font rl.Font, text string, rec rl.Rectangle, fontSize, spacing float32,
|
||||
wordWrap bool, tint rl.Color, selectStart, selectLength int32, selectTint, selectBackTint rl.Color) {
|
||||
|
||||
length := int32(len(text)) // Total length in bytes of the text, scanned by codepoints in loop
|
||||
|
||||
// TextOffsetY : Offset between lines (on line break '\n')
|
||||
// TextOffsetX : Offset X to next character to draw
|
||||
var textOffsetY, textOffsetX float32
|
||||
|
||||
scaleFactor := fontSize / float32(font.BaseSize) // Character rectangle scaling factor
|
||||
|
||||
// Word/character wrapping mechanism variables
|
||||
state := DrawState
|
||||
if wordWrap {
|
||||
state = MeasureState
|
||||
}
|
||||
|
||||
// StartLine : Index where to begin drawing (where a line begins)
|
||||
// EndLine : Index where to stop drawing (where a line ends)
|
||||
// LastK : Holds last value of the character position
|
||||
var startLine, endLine, lastk int32 = -1, -1, -1
|
||||
for i, k := int32(0), int32(0); i < length; i, k = i+1, k+1 {
|
||||
// Get next codepoint from byte string and glyph index in font
|
||||
codepoint, width := utf8.DecodeRuneInString(text[i:])
|
||||
codepointByteCount := int32(width)
|
||||
index := rl.GetGlyphIndex(font, codepoint)
|
||||
|
||||
// NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f)
|
||||
// but we need to draw all the bad bytes using the '?' symbol moving one byte
|
||||
if codepoint == 0x3f {
|
||||
codepointByteCount = 1
|
||||
}
|
||||
i += codepointByteCount - 1
|
||||
|
||||
var glyphWidth float32
|
||||
if codepoint != '\n' {
|
||||
chars := unsafe.Slice(font.Chars, font.CharsCount)
|
||||
if chars[index].AdvanceX == 0 {
|
||||
glyphWidth = unsafe.Slice(font.Recs, font.CharsCount)[index].Width * scaleFactor
|
||||
} else {
|
||||
glyphWidth = float32(chars[index].AdvanceX) * scaleFactor
|
||||
}
|
||||
|
||||
if i+1 < length {
|
||||
glyphWidth = glyphWidth + spacing
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: When wordWrap is ON we first measure how much of the text we can draw before going outside the rec container
|
||||
// We store this info in startLine and endLine, then we change states, draw the text between those two variables
|
||||
// and change states again and again recursively until the end of the text (or until we get outside the container).
|
||||
// When wordWrap is OFF we don't need the measure state so we go to the drawing state immediately
|
||||
// and begin drawing on the next line before we can get outside the container.
|
||||
if state == MeasureState {
|
||||
// TODO: There are multiple types of spaces in UNICODE, maybe it's a good idea to add support for more
|
||||
// Ref: http://jkorpela.fi/chars/spaces.html
|
||||
if (codepoint == ' ') || (codepoint == '\t') || (codepoint == '\n') {
|
||||
endLine = i
|
||||
}
|
||||
|
||||
if (textOffsetX + glyphWidth) > rec.Width {
|
||||
if endLine < 1 {
|
||||
endLine = i
|
||||
}
|
||||
|
||||
if i == endLine {
|
||||
endLine -= codepointByteCount
|
||||
}
|
||||
if (startLine + codepointByteCount) == endLine {
|
||||
endLine = i - codepointByteCount
|
||||
}
|
||||
|
||||
state = 1 - state // Toggle state between MeasureState and DrawState
|
||||
} else if (i + 1) == length {
|
||||
endLine = i
|
||||
state = 1 - state // Toggle state between MeasureState and DrawState
|
||||
} else if codepoint == '\n' {
|
||||
state = 1 - state // Toggle state between MeasureState and DrawState
|
||||
}
|
||||
|
||||
if state == DrawState {
|
||||
textOffsetX = 0
|
||||
i = startLine
|
||||
glyphWidth = 0
|
||||
|
||||
// Save character position when we switch states
|
||||
lastk, k = k-1, lastk
|
||||
}
|
||||
} else {
|
||||
if codepoint == '\n' {
|
||||
if !wordWrap {
|
||||
textOffsetY += float32(font.BaseSize+font.BaseSize/2) * scaleFactor
|
||||
textOffsetX = 0
|
||||
}
|
||||
} else {
|
||||
if !wordWrap && ((textOffsetX + glyphWidth) > rec.Width) {
|
||||
textOffsetY += float32(font.BaseSize+font.BaseSize/2) * scaleFactor
|
||||
textOffsetX = 0
|
||||
}
|
||||
|
||||
// When text overflows rectangle height limit, just stop drawing
|
||||
if (textOffsetY + float32(font.BaseSize)*scaleFactor) > rec.Height {
|
||||
break
|
||||
}
|
||||
|
||||
// Draw selection background
|
||||
isGlyphSelected := false
|
||||
if (selectStart >= 0) && (k >= selectStart) && (k < (selectStart + selectLength)) {
|
||||
rl.DrawRectangleRec(rl.Rectangle{
|
||||
X: rec.X + textOffsetX - 1,
|
||||
Y: rec.Y + textOffsetY,
|
||||
Width: glyphWidth,
|
||||
Height: float32(font.BaseSize) * scaleFactor,
|
||||
}, selectBackTint)
|
||||
isGlyphSelected = true
|
||||
}
|
||||
|
||||
// Draw current character glyph
|
||||
if (codepoint != ' ') && (codepoint != '\t') {
|
||||
col := tint
|
||||
if isGlyphSelected {
|
||||
col = selectTint
|
||||
}
|
||||
pos := rl.Vector2{
|
||||
X: rec.X + textOffsetX,
|
||||
Y: rec.Y + textOffsetY,
|
||||
}
|
||||
rl.DrawTextEx(font, string(codepoint), pos, fontSize, 0, col)
|
||||
}
|
||||
}
|
||||
|
||||
if wordWrap && (i == endLine) {
|
||||
textOffsetY += float32(font.BaseSize+font.BaseSize/2) * scaleFactor
|
||||
textOffsetX = 0
|
||||
startLine = endLine
|
||||
endLine = -1
|
||||
glyphWidth = 0
|
||||
selectStart += lastk - k
|
||||
k = lastk
|
||||
|
||||
state = 1 - state // Toggle state between MeasureState and DrawState
|
||||
}
|
||||
}
|
||||
|
||||
textOffsetX += glyphWidth
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue