github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/examples/ebiten/wrap/main.go (about) 1 package main 2 3 import "os" 4 import "log" 5 import "fmt" 6 import "image" 7 import "unicode/utf8" 8 9 import "golang.org/x/image/math/fixed" 10 import "github.com/Kintar/etxt" 11 import "github.com/hajimehoshi/ebiten/v2" 12 13 // The explanation of the example is displayed in the example itself 14 // based on the contents of this string: 15 const Content = "This example performs basic text wrapping in order to draw text " + 16 "within a delimited area. Additionally, it also shows how to embed " + 17 "the etxt.Renderer type in a custom struct that allows defining our " + 18 "own methods while also preserving all the original methods of " + 19 "etxt.Renderer.\n\nIn this case, we have added DrawInBox(text string, " + 20 "bounds image.Rectangle). Try to resize the screen and see how the text " + 21 "adapts to it. You may take this code as a reference and write your own " + 22 "text wrapping functions, as you often will have more specific needs." + 23 "\n\nIn most cases, you will want to add some padding to the bounds to " + 24 "avoid text sticking to the borders of your target text area." 25 26 // Type alias to create an unexported alias of etxt.Renderer. 27 // This is quite irrelevant for this example, but it's useful in 28 // practical scenarios to avoid leaking a public internal field. 29 type renderer = etxt.Renderer 30 31 // Wrapper type for etxt.Renderer. Since this type embeds etxt.Renderer 32 // it will preserve all its methods, and we can additionally add our own 33 // new DrawInBox() method. 34 type TextBoxRenderer struct{ renderer } 35 36 // The new method for TextBoxRenderer. It draws the given text within the 37 // given bounds, performing basic line wrapping on space " " characters. 38 // This is only meant as a reference: this method doesn't split on "-", 39 // very long words will overflow the box when a single word is longer 40 // than the width of the box, \r\n will be considered two line breaks 41 // instead of one, etc. In many practical scenarios you will want to 42 // further customize the behavior of this function. For more complex 43 // examples of Feed usages, see examples/ebiten/typewriter, which also 44 // has a typewriter effect, multiple colors, bold, italics and more. 45 // Otherwise, if you only needed really basic line wrapping, feel free 46 // to copy this function and use it directly. If you don't want a custom 47 // TextBoxRenderer type, it's trivial to adapt the function to receive 48 // a standard *etxt.Renderer as an argument instead. 49 // 50 // Notice that this function relies on the renderer's alignment being 51 // (etxt.Top, etxt.Left). 52 func (self *TextBoxRenderer) DrawInBox(text string, bounds image.Rectangle) { 53 // helper function 54 var getNextWord = func(str string, index int) string { 55 start := index 56 for index < len(str) { 57 codePoint, size := utf8.DecodeRuneInString(str[index:]) 58 if codePoint <= ' ' { 59 return str[start:index] 60 } 61 index += size 62 } 63 return str[start:index] 64 } 65 66 // create Feed and iterate each rune / word 67 feed := self.renderer.NewFeed(fixed.P(bounds.Min.X, bounds.Min.Y)) 68 index := 0 69 for index < len(text) { 70 switch text[index] { 71 case ' ': // handle spaces with Advance() instead of Draw() 72 feed.Advance(' ') 73 index += 1 74 case '\n', '\r': // \r\n line breaks *not* handled as single line breaks 75 feed.LineBreak() 76 index += 1 77 default: 78 // get next word and measure it to see if it fits 79 word := getNextWord(text, index) 80 width := self.renderer.SelectionRect(word).Width 81 if (feed.Position.X + width).Ceil() > bounds.Max.X { 82 feed.LineBreak() // didn't fit, jump to next line before drawing 83 } 84 85 // abort if we are going beyond the vertical working area 86 if feed.Position.Y.Floor() >= bounds.Max.Y { 87 return 88 } 89 90 // draw the word and increase index 91 for _, codePoint := range word { 92 feed.Draw(codePoint) // you may want to cut this earlier if the word is too long 93 } 94 index += len(word) 95 } 96 } 97 } 98 99 // ---- game and main code ---- 100 101 type Game struct { 102 txtRenderer *TextBoxRenderer 103 } 104 105 func (self *Game) Layout(w int, h int) (int, int) { return w, h } 106 func (self *Game) Update() error { return nil } 107 func (self *Game) Draw(screen *ebiten.Image) { 108 self.txtRenderer.SetTarget(screen) 109 self.txtRenderer.DrawInBox(Content, screen.Bounds()) 110 } 111 112 func main() { 113 // get font path 114 if len(os.Args) != 2 { 115 msg := "Usage: expects one argument with the path to the font to be used\n" 116 fmt.Fprint(os.Stderr, msg) 117 os.Exit(1) 118 } 119 120 // parse font 121 font, fontName, err := etxt.ParseFontFrom(os.Args[1]) 122 if err != nil { 123 log.Fatal(err) 124 } 125 fmt.Printf("Font loaded: %s\n", fontName) 126 127 // create cache 128 cache := etxt.NewDefaultCache(1024 * 1024 * 1024) // 1GB cache 129 130 // create and configure renderer 131 txtRenderer := &TextBoxRenderer{*etxt.NewStdRenderer()} 132 txtRenderer.SetCacheHandler(cache.NewHandler()) 133 txtRenderer.SetSizePx(16) 134 txtRenderer.SetFont(font) 135 txtRenderer.SetAlign(etxt.Top, etxt.Left) // important for this example! 136 137 // run the game 138 ebiten.SetWindowTitle("etxt/examples/ebiten/wrap") 139 ebiten.SetWindowSize(640, 480) 140 ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) 141 err = ebiten.RunGame(&Game{txtRenderer}) 142 if err != nil { 143 log.Fatal(err) 144 } 145 }