ebiten icon indicating copy to clipboard operation
ebiten copied to clipboard

text: a utility function to render texts with a left-upper position

Open hajimehoshi opened this issue 2 years ago • 39 comments

How to render texts with a left-upper position is a very frequently asked question. Having a utility function to do so would be a great feature. Let's think about APIs first.

See also: https://github.com/sedyh/ebiten-cheatsheet#center-text (thank you, @sedyh!)

hajimehoshi avatar Jun 15 '22 16:06 hajimehoshi

Sure thing. Do you have any ideas about how you'd like to implement this? I'm not really sure how else to go about it without introducing more to the public interface. Except maybe adding a parameter to to the 'Draw' or 'DrawWithOptions' function but that would introduce incompatibility.

KalebHawkins avatar Jun 18 '22 13:06 KalebHawkins

Hi,

I think introducing a new function(s) would be inevitable. Based on your PR #2150:

  • For example, how would x and j be rendered? Aren't their baselines different?
  • Should we consider vertical aligns at the same time?
  • Specifying image.Rectangle to calculate alignments would be preferable.
  • Can we have an enum instead of having multiple functions?

hajimehoshi avatar Jun 18 '22 13:06 hajimehoshi

For example, how would x and j be rendered? Aren't their baselines different?

I don't fully understand what you mean here. Sorry. Is j a typo for y?

Should we consider vertical aligns at the same time?

I don't think that is a bad idea. To clarify you are saying something along the lines of having a TopAlignLeft, TopCenter, TopAlignRight, CenterAlignLeft, Centered, CenterAlignRight, etc... Is that correct?

Specifying image.Rectangle to calculate alignments would be preferable.

I'm still really fresh to ebitengine, I thought text.BoundString() was taking the rectangle of the given text with the font options is that not the same thing?

Can we have an enum instead of having multiple functions?

I do like this idea better then what was in the pull request. Are you thinking something along the lines of what is below?

type TextAlignment int

const (
    ToptAlignLeft TextAlignment = iota
    TopAlignCentered
    TopAlignRight
    CenterAlignLeft
    Centered
    CenterAlignRight
    BottomAlignLeft
    BottomAlignCentered
    BottomAlignRight
)

func DrawAligned(dst *ebiten.Image, text string, face font.Face,  xSkew int, ySkew int, clr color.Color, align TextAlignment) {
    // ... Do alignment stuff with text.
}

The xSkew and ySkew would be an added offset of where the default destination of the alignment would be allowing for a little more flexibility of the placement of the text. That is something optional and if we want to keep it as simple as possible you may decide against them.

Personally, I like it but anyone newer coming into the API might would be confused. Let me know what you think.

Also if you can elaborate on what you mean with the image.Rectangle I can see what I can do there. I am very new to ebitengine, extremely new to game dev and intermediate with Go dev on my best days. So your patience and explanations are appreciated I am here to learn as much as can as well as contribute what I can.

KalebHawkins avatar Jun 18 '22 14:06 KalebHawkins

I don't fully understand what you mean here. Sorry. Is j a typo for y?

'y' is also fine. I meant some glyphs that have ascendent and/or descendent.

I'm still really fresh to ebitengine, I thought text.BoundString() was taking the rectangle of the given text with the font options is that not the same thing?

I meant a rectangle as a destination. In your suggestion, for example, the right edge would be the same as the right edge of the destination image. This might be useless when a use don't want to align to the image's edge.

Or, instead of a destination rectangle, a destination point would work and this would be enough. In this case, the text would be rendered based on the specified point. I'll draw a figure later.

IMO, I prefer specifying a point to a rectangle so far. This might be a little tricky but concise.

I do like this idea better then what was in the pull request. Are you thinking something along the lines of what is below?

Yes, the direction seems fine. I think we need more refine. I'll put my suggestion later (maybe tomorrow. Thank you for your patience).

The xSkew and ySkew would be an added offset ...

If we specify a destination rectangle or a destination point, we don't need them, right?

Also if you can elaborate on what you mean with the image.Rectangle I can see what I can do there. I am very new to ebitengine, extremely new to game dev and intermediate with Go dev on my best days. So your patience and explanations are appreciated I am here to learn as much as can as well as contribute what I can.

Thank you for trying to contribute to Ebitengine! I appreciate your PR!

hajimehoshi avatar Jun 18 '22 15:06 hajimehoshi

I meant a rectangle as a destination. In your suggestion, for example, the right edge would be the same as the right edge of the destination image. This might be useless when a use don't want to align to the image's edge.

Or, instead of a destination rectangle, a destination point would work and this would be enough. In this case, the text would be rendered based on the specified point. I'll draw a figure later.

I think I understand. I am placing the text based off the dot-position. So if your dst image is the logical screen and you specified Centered as your placement, assuming . is your string, the . should appear in the center of the screen.

Which seems to coincide with the documentation of the text.Draw() function.

To further that explanation lets say pass a dst image other then screen, for example an image created with *ebiten.NewImage(16, 16), either way it is an *ebiten.Image you would specify the location of the the image and then pass that image to text.DrawAligned(). Assuming you used Centered in your text.DrawAligned() function it would place the text passed into the center of the dst image or in this case (8, 8) of the *ebiten.NewImage(16, 16).

I hope all that makes sense...

The xSkew and ySkew are just added offsets so if you wanted to place text at say a 3rd of the screen height you could pass -screenHeight/3 as the ySkew and it would move it up the center of the screen. Again this assumes Centered is the option used. But it would work the same with the other enum options.

It still be better to leave the skews out though since the same thing can be accomplished with little math from text.Draw() but I think of it as a convenience feature.

KalebHawkins avatar Jun 19 '22 16:06 KalebHawkins

I think I understand. I am placing the text based off the dot-position. So if your dst image is the logical screen and you specified Centered as your placement, assuming . is your string, the . should appear in the center of the screen.

I'm not talking about '.' position here. The specified point (or the rectangle's left-upper position) would be used to locate the text that left-upper position is the same as the specified point, for example.

In this case, xSkew and ySkew are not needed as the specified position can be changed.

Does this make sense?

hajimehoshi avatar Jun 20 '22 03:06 hajimehoshi

text

With a specified point, the text can be placed like above (left-upper, center-middle, right-lower). There are 9 possibilities.

hajimehoshi avatar Jun 20 '22 03:06 hajimehoshi

Should the specified point come from the 'dst' image passed then?

In your left-upper and right-lower examples though, the left-upper example places the text on the right side of the screen and the right-lower example places text to the left side of the screen. That seems a little counter intuitive doesn't it?

I would think aligning text would work like in most text editors like Word.

If you are designing something with multiple lines of centered or left aligned text how do you propose you move it up or down the 'y' axis of the screen? Would that just be broken up through new lines. I can give an example if needed.

Either way I can make it happen in the PR.

KalebHawkins avatar Jun 20 '22 03:06 KalebHawkins

Should the specified point come from the 'dst' image passed then?

Sorry but I don't understand this question. My intention is pass a position separately from the destination image.

In your left-upper and right-lower examples though, the left-upper example places the text on the right side of the screen and the right-lower example places text to the left side of the screen. That seems a little counter intuitive doesn't it?

Well, this might be a little counterintuitive. So specifying a rectangle instead of a point might be intuitive, but my concern of passing a rectangle is redundancy.

If you are designing something with multiple lines of centered or left aligned text how do you propose you move it up or down the 'y' axis of the screen? Would that just be broken up through new lines. I can give an example if needed.

Good question. My intention was to center the entire text with multiple lines, if we specify centering.

hajimehoshi avatar Jun 20 '22 04:06 hajimehoshi

Sorry but I don't understand this question. My intention is pass a position separately from the destination image.

No worries that answered my question. We should pass the position as a parameter to the function then.

Well, this might be a little counterintuitive. So specifying a rectangle instead of a point might be intuitive, but my concern of passing a rectangle is redundancy.

I agree I feel like creating a separate rectangle object is a little overkill when all we want to do is put some text on the screen or within an empty image or something.

Let me draw up so code and display some examples and we can see which way seems the simplest and most intuitive. I'll post some screen shots here along with the code and see what you like best. I appreciate the patience with this and my probably not so great questions :).

KalebHawkins avatar Jun 20 '22 13:06 KalebHawkins

I drew up some code with some examples of the skew. I'll go ahead and post that here. I'll work on something to show based on a specified point as you mentioned. Not entirely sure how to implement that just yet.

Note: The original PR was closed on accident... I'll open another once we decide the design.

Below is an example using the text.DrawAligned()

func (g *Game) Draw(screen *ebiten.Image) {
	text.DrawAligned(screen, "Left-Upper", mplusNormalFont, 0, 0, color.White, text.TopAlignLeft)
	text.DrawAligned(screen, "Left-Center", mplusNormalFont, 0, 0, color.White, text.CenterAlignLeft)
	text.DrawAligned(screen, "Left-Bottom", mplusNormalFont, 0, 0, color.White, text.BottomAlignLeft)

	text.DrawAligned(screen, "Left-Upper x, y skew", mplusNormalFont, 20, 30, color.White, text.TopAlignLeft)

	text.DrawAligned(screen, "Right-Upper", mplusNormalFont, 0, 0, color.White, text.TopAlignRight)
	text.DrawAligned(screen, "Right-Center", mplusNormalFont, 0, 0, color.White, text.CenterAlignRight)
	text.DrawAligned(screen, "Right-Bottom", mplusNormalFont, 0, 0, color.White, text.BottomAlignRight)

	text.DrawAligned(screen, "Right-Bottom x, y skew", mplusNormalFont, -20, -30, color.White, text.BottomAlignRight)

	text.DrawAligned(screen, "Center-Top", mplusNormalFont, 0, 0, color.White, text.TopAlignCentered)
	text.DrawAligned(screen, "Centered", mplusNormalFont, 0, 0, color.White, text.Centered)
	text.DrawAligned(screen, "Center-Bottom", mplusNormalFont, 0, 0, color.White, text.BottomAlignCentered)

	text.DrawAligned(screen, "Centered with ySkew 1", mplusNormalFont, 0, -screenHeight/3, color.White, text.Centered)
	text.DrawAligned(screen, "Centered with ySkew 2", mplusNormalFont, 0, screenHeight/3, color.White, text.Centered)

}

This is the resulting output.

image

I think this method makes it really simple to through together a very basic main menu prototype or even a simple HUD prototype with minimal code and well... math (nobody likes math right? lol). Another example.

func (g *Game) Draw(screen *ebiten.Image) {
	text.DrawAligned(screen, "Welcome to Game", mplusNormalFont, 0, -screenHeight/3, color.White, text.Centered)
	text.DrawAligned(screen, "Press (P) to Play", mplusNormalFont, 0, -screenHeight/4, color.White, text.Centered)
	text.DrawAligned(screen, "Press (Q) to Quit", mplusNormalFont, 0, -screenHeight/5, color.White, text.Centered)

	text.DrawAligned(screen, "Health: 100", mplusNormalFont, 20, 10, color.White, text.TopAlignLeft)
	text.DrawAligned(screen, "Stamina: 75", mplusNormalFont, 0, 10, color.White, text.TopAlignCentered)
	text.DrawAligned(screen, "Gold: 1000", mplusNormalFont, -20, 10, color.White, text.TopAlignRight)
}

image

KalebHawkins avatar Jun 20 '22 15:06 KalebHawkins

Thanks, but the position is still based on the destination image's rectangle, right? What I suggested is to specify a point (or a rectangle) to adjust the text position. I don't want to use the destination image's rectangle as this is a little hard to use.

hajimehoshi avatar Jun 20 '22 15:06 hajimehoshi

For example, the left-bottom goes to the bottom of the target image. It is possible to adjust the position by the skews, but I'm not sure this is the correct direction.

hajimehoshi avatar Jun 20 '22 15:06 hajimehoshi

Thanks, but the position is still based on the destination image's rectangle, right?

That is correct.

I believe what you are describing can accomplish this already using text.Draw() correct?. Are you basing the point off a location of the text's rectangle or a point in the dst image? From your screenshot above it seems like you are basing off the text's location.

Maybe we can discuss in discord further for quicker clarification on my end? Unless you want to keep design decisions here of course either way is fine with me.

KalebHawkins avatar Jun 20 '22 17:06 KalebHawkins

I believe what you are describing can accomplish this already using text.Draw() correct?.

A little different. text.Draw uses the specified point as the dot position. My suggestion is to use this as the left-upper position of the text for example.

Are you basing the point off a location of the text's rectangle or a point in the dst image? From your screenshot above it seems like you are basing off the text's location.

A point in the dst image. The text will be located based on the position. Does this make sense?

Maybe we can discuss in discord further for quicker clarification on my end? Unless you want to keep design decisions here of course either way is fine with me.

Sure but I'm gonna bed soon. Time zone is hard. I'll leave my idea of the APIs later.

hajimehoshi avatar Jun 20 '22 17:06 hajimehoshi

Yesterday I was thinking about how to match with the existing APIs. The existing APIs are

  • func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color)
  • func DrawWithOptions(dst *ebiten.Image, text string, face font.Face, options *ebiten.DrawImageOptions)

So, introducing a new function for both a non-option version and an option version would be painful...

Would it make sense to add a new function to adjust the position like this?

  • AdjustPosition(text string, face font.Face, alignment Alignment, basePointX, basePointY int) (int, int)

Then a user can use like this:

x, y := AdjustPosition(str, face, text.UpperLeft, 100, 100)
text.Draw(screen, str, face, x, y, color.White)

or

x, y := AdjustPosition(str, face, text.UpperLeft, 100, 100)
op := ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(x), float64(y))
text.DrawWithOptions(screen, str, face, op)

hajimehoshi avatar Jun 21 '22 12:06 hajimehoshi

The returned x, y value is some point on the text's rectangle correct? In the case of UpperLeft the top left corner. I don't understand the need for the base point parameters unless they are just an added offset to the point on the text's rectangle..

I think we may ultimately be trying to accomplish two different things when it comes to the text alignment.

From your previous examples though UpperLeft would place a point on the top left corner of the text and then you would place the text depending on that point. Is that right?

KalebHawkins avatar Jun 21 '22 13:06 KalebHawkins

The returned x, y value is some point on the text's rectangle correct?

No, these are a new 'dot' position in the destination image rectangle that can be passed to Draw.

I don't understand the need for the base point parameters unless they are just an added offset to the point on the text's rectangle..

The intention is that the user can render a text at (100, 100), that matches the text's upper-left position.

From your previous examples though UpperLeft would place a point on the top left corner of the text and then you would place the text depending on that point. Is that right?

I think it's a little different: AdjustPosition just returns a new dot position rather than a text position. Sorry this might be a little tricky.

hajimehoshi avatar Jun 21 '22 15:06 hajimehoshi

That all makes sense. Let me see if I can get something drawn up. I'll post some examples as soon as I can.

KalebHawkins avatar Jun 21 '22 15:06 KalebHawkins

So, if the font's ascender [1] is 10 pixel and y offset of 'dot' is 1, AdjustPosition(str, face, text.UpperLeft, 100, 100) will return 110 and 101.

AdjustDotPosition might be better than AdjustPosition by the way.

I'll post some examples as soon as I can.

Thank you!

[1] https://en.wikipedia.org/wiki/Ascender_(typography)

hajimehoshi avatar Jun 21 '22 15:06 hajimehoshi

I think I MAY have something worked out... I still don't feel like I did it entirely right because I am not exactly sure how I can obtain the ascender. I did make something that I believe emulates what you describes.

func (g *Game) Draw(screen *ebiten.Image) {
	halfHeight := screenHeight / 2
	halfWidth := screenWidth / 2

	dx, dy := text.AdjustDotPosition("UpperLeft", mplusNormalFont, text.UpperLeft, halfWidth, halfHeight)
	text.Draw(screen, "LeftUpper", mplusNormalFont, dx, dy, color.White)
	dx, dy = text.AdjustDotPosition("UpperRight", mplusNormalFont, text.UpperRight, halfWidth, halfHeight)
	text.Draw(screen, "UpperRight", mplusNormalFont, dx, dy, color.White)
	dx, dy = text.AdjustDotPosition("LowerLeft", mplusNormalFont, text.LowerLeft, halfWidth, halfHeight)
	text.Draw(screen, "LowerLeft", mplusNormalFont, dx, dy, color.White)
	dx, dy = text.AdjustDotPosition("LowerRight", mplusNormalFont, text.LowerRight, halfWidth, halfHeight)
	text.Draw(screen, "LowerRight", mplusNormalFont, dx, dy, color.White)

	vLine := ebiten.NewImage(1, screenHeight)
	vopts := &ebiten.DrawImageOptions{}
	vopts.ColorM.Translate(0, 128, 128, 255)
	vopts.GeoM.Translate(screenWidth/2, 0)

	hLine := ebiten.NewImage(screenWidth, 1)
	hopts := &ebiten.DrawImageOptions{}
	hopts.ColorM.Translate(0, 128, 128, 255)
	hopts.GeoM.Translate(0, screenHeight/2)

	screen.DrawImage(hLine, hopts)
	screen.DrawImage(vLine, vopts)
}

image

dx, dy := text.AdjustDotPosition("Centered", mplusNormalFont, text.Center, halfWidth, halfHeight)
text.Draw(screen, "Centered", mplusNormalFont, dx, dy, color.White)

image

dx, dy := text.AdjustDotPosition("LeftCentered", mplusNormalFont, text.CenterLeft, halfWidth, halfHeight)
text.Draw(screen, "LeftCentered", mplusNormalFont, dx, dy, color.White)

image

dx, dy := text.AdjustDotPosition("CenterRight", mplusNormalFont, text.CenterRight, halfWidth, halfHeight)
text.Draw(screen, "CenterRight", mplusNormalFont, dx, dy, color.White)

image

I think the only problem I have with this is that you have to specify the string twice. I guess that can be solved with variables though. Let me know what you think. I may still be doing this completely wrong.

KalebHawkins avatar Jun 21 '22 20:06 KalebHawkins

Thank you for working! I'll take a look later.

Just a quick reply:

because I am not exactly sure how I can obtain the ascender

face.Metrics().Ascent?

https://pkg.go.dev/golang.org/x/image/font#Metrics

hajimehoshi avatar Jun 22 '22 06:06 hajimehoshi

I think it would be helpful to look at my etxt package more closely to see how I solved these problems. There's a ton of relevant knowledge there, it's well documented and the critical code is clean. The API is quite different as there are also many other parameters and right to left text and individual glyph handling and all that, but it should be a good design reference. Even if we have the align configuration or others (e.g: text quantization, color, size) in an "options" struct instead of a renderer type with aligns, the general concerns are the same.

You can copy any implementation details that you want from etxt, the real problem is understanding all possible configurable elements when drawing text and determining what's the intended scope for Ebitengine.

tinne26 avatar Jun 22 '22 07:06 tinne26

Thank for the advice, but rather than implementation, the (my) current focus is how to reconcile with the current APIs.

hajimehoshi avatar Jun 22 '22 07:06 hajimehoshi

Yeah, that's exactly what I said in the last line, that implementation is not a problem, API design is. But my point is that even for the API there are parts that are being missed in this discussion. For example, etxt uses two separate types for the vertical and the horizontal aligns. The current discussion is missing baseline aligns. Also, vertical aligning needs to carefully consider the font metrics vs text bounds. Sedyh's version of text centering is not suitable for all use-cases: for example, when aligning text vertically on multiple buttons, you want all button texts to be aligned from the baseline, not perfectly centered from their bounds. And so on.

tinne26 avatar Jun 22 '22 08:06 tinne26

@KalebHawkins

I did make something that I believe emulates what you describes.

Yeah, the figures are actually what I wanted. Thank you very much!

I think the only problem I have with this is that you have to specify the string twice. I guess that can be solved with variables though. Let me know what you think. I may still be doing this completely wrong.

Right, this redundancy seems inevitable so far... So, in addition to adding AdjustDotPostion, what about adding a utility function only for Draw? I'm sorry we are going back and forth.

// I've not determined the argument order so they might be changed later
func DrawWithAlignment(dst *ebiten.Image, text string, face font.Face, alignment Alignment, x, y int, clr color.Color) {
    x, y := AdjustDotPosition(text, face, alignment, x, y)
    Draw(dst, text, face, x, y, clr)
}

So if people want to use DrawWithOptions, they can still use AdjustDotPosition. The code would be verbose but it's ok as this is a kind of advanced usages.

hajimehoshi avatar Jun 22 '22 08:06 hajimehoshi

@tinne26

But my point is that even for the API there are parts that are being missed in this discussion.

Adding drastic changes to the current package is out of scope in this issue. Let's create another issue and discuss there.

hajimehoshi avatar Jun 22 '22 11:06 hajimehoshi

The current discussion is missing baseline aligns. Also, vertical aligning needs to carefully consider the font metrics vs text bounds.

Well, this seems worth discussing in this issue (sorry I missed this first). I'll take a look at other specifications like CSS.

hajimehoshi avatar Jun 22 '22 11:06 hajimehoshi

For horizontal aligns, there are 8 types: https://developer.mozilla.org/en-US/docs/Web/CSS/text-align For vertical aligns, there are 8 types: https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align

Random notes:

  • Of course we don't need to implement all of them.
  • Implementing justify might be tricky though it is challenging... In this case, we would need to specify a rectangle instead of a point.
  • In CSS, there are the (inline-)block type and the block type and these have different notions. For the block type, we might need another spec like flex-box.
  • To make things simplify, what about these alignments:
    • Horizontal
      • Left
      • Center
      • Right
    • Vertical
      • Top
      • Baseline (of the first line?)
      • Middle (of the entire text block?)
      • Bottom

Text renderings are complicated as @tinne26 said.

hajimehoshi avatar Jun 22 '22 15:06 hajimehoshi

Yeah, agree, I don't think CSS Sub and Text-Top vertical aligns are relevant for Ebitengine, it will only add noise. And yes, baseline has to be based on the first line in case of multiple lines, and middle has to be based on the entire text block.

Horizontal aligns are clear, and for vertical the only thing we may want to consider is separating Middle into Center and CenterLine, as I previously mentioned when discussing Sedyh's approach. To make it clearer: Center would center text based on string bounds, while CenterLine would center text based on line height (names could be improved). Notice that these different approaches would produce very different results if text blocks start or end with line breaks, or have all lines starting or ending with spaces or other empty glyphs.

A practical example of why this might be relevant:

  • If you have two buttons of the same size, one next to the other, and in one you type "mama" and on the other you type "papa", centering based on bounds will make the button texts look misaligned, as the bounds for "mama" and "papa" have different heights. "baba" and "yaga" would look even worse. Using CenterLine, instead, would still keep the text vertically centered in the buttons but both words would share the same y coordinate for their baseline.

This may still be too noisy and tricky, but even if we decide against this separation, I'd argue in favor of using the CenterLine approach for centering instead of the bounds-based approach, as it's generally more consistent. This is what etxt does, and in general browsers and text editors do not use bounds directly but rather boxes based on font metrics and glyph advances. See etxt/renderer.SelectionRect.

In general, I'm a proponent of using these boxes instead of strict glyph outline bounds, as bounds are very tricky to work with (e.g. https://github.com/hajimehoshi/ebiten/issues/1992). So, I prefer SelectionRect metrics to BoundString for most use-cases. They all have their drawbacks, but it's a point deserving consideration.

Besides that, I think that adding a single AdjustDotPosition call for the API sounds fine, but maybe we could rename it AdjustDot or even AlignDot depending on the names of the align constants.

tinne26 avatar Jun 23 '22 07:06 tinne26

Earlier today I remembered something important that we have been missing in this dicussion:

  • The AdjustDotPosition approach has a big limitation: right-aligned text is not truly possible when the text has multiple lines. The right-aligning will align the multiline block, but without more information than a (x, y) coordinate, text.Draw can't do proper right-aligning. Technically single-line right align will still work properly, but this behavior on multi-line blocks will be very confusing for regular users.

If this issue is considered significant enough, we may have to rethink the approach, which then would most likely require breaking changes or additional new functions, like DrawWithOptions or similar (edit: sorry, this collides with the already existing function, I mean a different one with draw options for the align and all that). I actually wrote a design for such an approach for Ebitengine, but it included more concepts that were out the scope of this issue (but I can look for it and share it if we have to go back to the design board due to the right-align issue).

tinne26 avatar Jul 15 '22 20:07 tinne26

right-aligned text is not truly possible when the text has multiple lines. The right-aligning will align the multiline block, but without more information than a (x, y) coordinate, text.Draw can't do proper right-aligning.

Is that true? The second and the following lines can be aligned to the specified right edge, right?

hajimehoshi avatar Jul 16 '22 04:07 hajimehoshi

It can be but not with the AdjustDotPosition approach alone and without DrawWithAlignment. With that, you only have (x, y) information passed to Draw, when you would require both (x, y) and the align information (horizontal align info would be enough, vertical is not strictly necessary). Otherwise you can't know the x at which to reset the next line. In fact, this also happens for centered text. You can't do left/center/right multi-line aligns only with (x, y) information. So you can't combine AdjustDotPosition with DrawWithOptions. Nor AdjustDotPosition with Draw. It doesn't work for multi-line right and center aligns. DrawWithAlignment would work by itself, but then you are not covering the DrawWithOptions functionality. In conclusion, AdjustDotPosition may be more harmful than helpful and not a good idea.

Edit: basically, that what you said on https://github.com/hajimehoshi/ebiten/issues/2143#issuecomment-1161694721 is not possible, and like Kaleb showed, you still need to pass the align information to draw then, so you can't compose those functions without breaking the API.

tinne26 avatar Jul 16 '22 07:07 tinne26

Apprently we need more discussion to determine the API...

@KalebHawkins I'm sorry we cannot determine the APIs yet and need more time, and thank you for trying to contribute to Ebitengine.

hajimehoshi avatar Jul 21 '22 06:07 hajimehoshi

So, I see four main ways forward:

  • Leave the API as it is (if anyone needs the features you can always refer them to etxt).
  • Provide the AdjustDot function anyway even if it won't work for multi-line centered and right aligned text. This seems hacky as it doesn't really solve the problem, but it's still helpful in many cases and composable.
  • Add DrawCenter and DrawRight, and leave Draw acting as the "DrawLeft" function. As a downside, we can't compose with DrawWithOptions and others. This seems ugly and limited but at least it's not incorrect. Alternatively, add a single DrawAligned function that's the same as Draw but takes an extra align parameter.
  • Redesign the text API for v3 in addition to any of the other three approaches. I can make an API proposal for this if there's any interest for it. The alternative minimal changes would involve adding an align parameter to all the drawing functions.

tinne26 avatar Jul 22 '22 07:07 tinne26

Is it possible to add a new field for DrawTextOptions?

sedyh avatar Jul 22 '22 08:07 sedyh

No because ebiten.DrawImageOptions is used, the same type as for regular draw operations (assuming you were referring to text.DrawWithOptions). I mean, it's technically possible to add a field and it wouldn't collide with image draw operations, but it would be beyond hacky. But yeah, an API redesign would almost definitely use a new text.Options type that included all that information (font, color, align, even size to get rid of font faces if going etxt's way, and maybe additional options like horizontal interspacing, line height or interspacing, etc).

tinne26 avatar Jul 22 '22 09:07 tinne26

But yeah, an API redesign would almost definitely use a new text.Options type that included all that information (font, color, align, even size to get rid of font faces if going etxt's way, and maybe additional options like horizontal interspacing, line height or interspacing, etc).

I agree with it. I think this is the most correct option.

sedyh avatar Jul 22 '22 10:07 sedyh

Apprently we need more discussion to determine the API...

@KalebHawkins I'm sorry we cannot determine the APIs yet and need more time, and thank you for trying to contribute to Ebitengine.

No worries at all! I had gotten really busy with work so I wasn't able to keep up with the conversation. Funny how something can sound simple then get complicated really fast lol.

KalebHawkins avatar Jul 22 '22 13:07 KalebHawkins