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  }