Ren'Py Truncating displayable text to fit its container

ZLZK

Member
Modder
Jul 2, 2017
276
626
I want a string in text to be automatically cut and end with ... when it's too long.

I have managed to pull it off,
but I don't know if my solution have any consequences,
of if there isn't a better way to do this.

Code:
You don't have permission to view the spoiler content. Log in or register now.

So is my code fine or not?

By the way, this behavior should be in game as a another layout,
but I'm not the one to make it happen.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,368
15,282
I want a string in text to be automatically cut and end with ... when it's too long.
Why ?

I understand it for something like my variable viewer, because I have absolutely no control over the length of the text it will display, and there's still ways to get the full content.
But whatever if you're making a mod or a game, you are in control of the text, and it's the only way to see it. Therefore what is the interest to put in your mod or game a text that no one will read entirely ?


This being said, you should link your code to , it's what this configuration variable is made for.
 

ZLZK

Member
Modder
Jul 2, 2017
276
626
Therefore what is the interest to put in your mod or game a text that no one will read entirely ?
It's to be able to display everything at once, without text overlapping.
You can then display full text in a single element viewer.

There is always a limit to anything.
If text won't fit on screen, you won't see it anyway.
By limiting it yourself you can deal with it.

This being said, you should link your code to , it's what this configuration variable is made for.
I think config.replace_text is a bit excessive,
it apply to all displayable text, but usage of my function will be scarce.
Also I wasn't able to pass kwargs to it.
And I need them to evaluate text width.

EDIT:
I mean the usage of this is when text based statement receives string of unknown length.
And when text must be in in 1 line. (layout="nobreak")
But also must not go over container. (my function)
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,368
15,282
It's to be able to display everything at once, without text overlapping.
You can then display full text in a single element viewer.
But... :/ You don't display "everything at once", since you truncate a part of your text to make it fit...


EDIT:
I mean the usage of this is when text based statement receives string of unknown length.
And when text must be in in 1 line. (layout="nobreak")
But also must not go over container. (my function)
I got it that way. And it's precisely to this that my answer apply.

When you are writing your text, you know that "here" you have a length constraint, and you write your text for it to fit.
The only case where it's needed is when you have no control over the length of the text. In my variable viewer, the text is the content of the variable. When it's a dict or a list, this content can grow really quickly. Then, yes, truncating make sense. And it's also better than a viewport, because the representation of a list or dict can perfectly need thousands letters (I know a game where a list is over 250,000 characters).
Technically, it shouldn't not be a problem by itself for Ren'Py to have a hundreds thousands long text in a viewport. But practically it would surely slowdown the game, and anyway it would be unreadable for the player. Therefore, the text is truncated, and if the player click, the list/dict will be displayed element by element. What will show its whole content and be easier to read.

But when you are the one who wrote the text, whatever if it's for a mod or a game, it's something radically different.
You know the constraints imposed by the interface design, and you write your text for it to always fit. It's the reason why most games (even AAA titles) have a letters limit when you are offered the possibility to personalize a name ; all the texts in the game are wrote for them to fit with names up to this limit.
Having to rely on a truncation feature, especially when it will rarely be used, yet only in case you know that you know in advance, is the sign of bad design, not of well thought game.

Anyway there's viewports to handle those cases. But most of the time, the game design should rely on a summary/description sentence, then display the whole text "on click".
Some kind of "she have a bad opinion about you", then as detailed explanation, "you're always late on your dates, and she wonder if you hangout with her out of love, or just because you want to fuck her."

It's one of the first rules when you make a game: "If it don't fit, rewrite it".
 

ZLZK

Member
Modder
Jul 2, 2017
276
626
But... :/ You don't display "everything at once", since you truncate a part of your text to make it fit...
I meant displaying all elements, not all their content.

But when you are the one who wrote the text, whatever if it's for a mod or a game, it's something radically different.
You know the constraints imposed by the interface design, and you write your text for it to always fit. It's the reason why most games (even AAA titles) have a letters limit when you are offered the possibility to personalize a name ; all the texts in the game are wrote for them to fit with names up to this limit.
Having to rely on a truncation feature, especially when it will rarely be used, yet only in case you know that you know in advance, is the sign of bad design, not of well thought game.
You are wrongly assuming that the text will always be the same.
I have made a screen where you give a list of strings to it and it's displaying them one by one.
I could make character limit like you have mentioned.
But different fonts have different width, so I made my function.

So you are telling me that when I do a mod I need to provide my own font?
-I don't want to.

And yeah still if it's a font that each character have different width,
you won't make character limit fit perfectly.
Since amount of characters and their width are two different things.
That's why I compare text width not string length.

I didn't consider that given string can be absurdly big.
So I guess I still need to make upper character limit anyway.

EDIT:
I'm working on universal choice screen that needs to work with most Ren'Py versions.
Quite the troublesome task, since there are different games and different Ren'Py.
And I can't use fixed/improved code in older versions...
This is what I have done so far.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,368
15,282
But different fonts have different width, so I made my function.
*sigh*
Firstly, it's what scaling is for. You get the size of a "W" and a "j" with the current font, and you adjust the font size accordingly to this.
Secondly you have no obligation to use the default styles.


So you are telling me that when I do a mod I need to provide my own font?
No, I'm telling you that when you're doing a mod, you need to think twice more than when you do a game. And double it again when you want to do an universal mod.
The result need to be consistent, including in look, cover as much exceptions as possible, and be idiot proof because you never know what a game developer can come with.

This being said, you don't need to provide a font, there's two (well one if you want a deep backward compatibility) always present.


EDIT:
I'm working on universal choice screen that needs to work with most Ren'Py versions.
Quite the troublesome task, since there are different games and different Ren'Py.
And I can't use fixed/improved code in older versions...
This is what I have done so far.
*sigh*

Firstly because what you want to do absolutely don't need so many over complicated lines.
Just addressing the point of this thread (the text shouldn't overflow the display zone):
/!\ wrote on the fly, so there's perhaps a typo or two /!\
Python:
screen altChoice( items ):

    #  Need to be here, because the rendering part is not available at init time.
    #  Most letters have the same width than 'n', the bigger and smaller will compensate
    # each others.
    if not hasattr( store, "maxLetters" ):
            $ store.maxLetters = ( config.screen_width - 200 ) / ( renpy.text.text.Text( "n", style="choice_button_text" ).render( 500,500, 1, 1 ).get_size()[0] )
            $ store.halfLetters = int( ( store.maxLetters - 3 ) / 2 )

    default tt = Tooltip("")

    style_prefix "choice"

    vbox:

        for i in items:
            if len( i.caption ) > maxLetters:
                textbutton i.caption[:halfLetters] + "[[...]" + i.caption[0-halfLetters:]:
                    action i.action
                    hovered tt.Action( i.caption )
            else:
                textbutton i.caption:
                    action i.action

    if tt.value != "":
        frame style "empty":
            ypos 1075
            padding ( 20, 35 )
            text tt.value style "say_dialogue"
Secondly, you define the style for all the screen statements you use. So you already have a full control over the width and height for every elements. For this you just need to use DejaVu as font for the text, and you'll not have anymore to compute the size.
 
  • Like
Reactions: gojira667 and ZLZK

ZLZK

Member
Modder
Jul 2, 2017
276
626
Firstly because what you want to do absolutely don't need so many over complicated lines.
I didn't do styles yet, but the only difference would be-
that they would be declared once instead of in each statement.
But it's only shorter code and does the same thing.
But for convenience I will move them out of screen.

I guess figuring out max length once instead of doing it each time it's needed might be better.
I didn't test your code and don't need to, because I understand what it does.
It has two flaws I'm aware of:
1. It doesn't work with translations if text doesn't fit.
2. It does need a variable for each different display zone and each different font size.

Since my function isn't without flaws, I might as well use your solution instead.
I guess I will make a global dict and a function that will be used in default screen statement.
Function parameters will be max_width and font_size + font_path or style.
The function will use current screen name as a key reference to that dict.
If key won't be in, it will use your solution to get value and insert it to that dict.
Now since the key is in, it will return value of it.

All this explaining made me do it:
You don't have permission to view the spoiler content. Log in or register now.
I guess we are done.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,368
15,282
I didn't do styles yet, but the only difference would be-
that they would be declared once instead of in each statement.
Or you could prefer to not have an uselessly over complicated life, and use the default font that come with Ren'Py.


1. It doesn't work with translations if text doesn't fit.
Nothing that can't be easily fixed. And I'll jump on the occasion to present another way to solve your issue:
/!\ still wrote on the fly, so still possible typos /!\
Python:
init python:

    def menuEx( items, *args, **kwargs ):

        if not hasattr( store, "maxLetters" ):
            store.maxLetters = ( config.screen_width - 200 ) / ( renpy.text.text.Text( "n", style="choice_button_text" ).render( 500,500, 1, 1 ).get_size()[0] )
            store.halfLetters = int( ( store.maxLetters - 3 ) / 2 )

        for ( t, v ) in items:
            if len( renpy.translation.translate_string( t ) ) > maxLetters:
                v.kwargs["short"] = renpy.translation.translate_string( t )[:halfLetters] + "[[...]" + renpy.translation.translate_string( t )[0-halfLetters:]

        return renpy.display_menu( items, *args, **kwargs )

    store.menu = menuEx

screen altChoice( items ):

    default tt = Tooltip("")

    style_prefix "choice"

    vbox:

        for i in items:
            if "short" in i.kwargs:
                textbutton i.kwargs["short"]:
                    action i.action
                    hovered tt.Action( i.caption )
            else:
                textbutton i.caption:
                    action i.action

    if tt.value != "":
        frame style "empty":
            ypos 1075
            padding ( 20, 35 )
            text tt.value style "say_dialogue"

2. It does need a variable for each different display zone and each different font size.
Your screen need 18 variables, and you complain because you would need to use a dict in place of a direct integer for maxLetters ?

What do you have against simplicity ?
 

ZLZK

Member
Modder
Jul 2, 2017
276
626
Or you could prefer to not have an uselessly over complicated life, and use the default font that come with Ren'Py.
I was talking about styles in general.
Because I don't see the difference,
if you use directly style properties or use style instead.
I will do font property later.

Nothing that can't be easily fixed. And I'll jump on the occasion to present another way to solve your issue:
It's same thing done differently...

Your screen need 18 variables, and you complain because you would need to use a dict in place of a direct integer for maxLetters ?
Most of them are style property values.
I meant for each other different statement you need to make another variable,
instead of having one function.
The dict I have done is solution to it.

What do you have against simplicity ?
What do you even mean?
I have remade your solution into reusable code for each different statement.
Literally changed multiple variables into one dict.

Did you not consider that I will need to display other text in different size and in different container?
Meaning declaring another maxLetters variable.
So I have done a dict to hold it there.

My code is also same thing done differently.
I don't see how my preference in a way it's done,
have anything to do with this.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,368
15,282
I was talking about styles in general.
Because I don't see the difference,
if you use directly style properties or use style instead.
I will do font property later.
And I was talking about you not starting by what, in the present case, condition everything.


It's same thing done differently...
No, it's solving what you where complaining about.

You pointed the fact that my example wasn't taking count of the translation, and the second example is taking count of it. It's the size of the sentence, translated if needed, that is tested, and it's the sentence (almost) like Ren'Py will display it, that is shortened.
And if I said "almost", it's not due to the translation, but because Ren'Py is doing something else before showing effectively displaying the string ; something that you didn't cared about and that I'll let you figure out for reasons.


Most of them are style property values.
And 12 of them are constants that don't need to be computed more than once ; at init time right before you define your styles, or at worse as config.start_callbacks callback.


What do you even mean?
I mean that each time I look at code you write, I have a headache due to its useless complexity.
You code the way you want, and design this code like you want it, but most of the time it could be way more simple. And, no, "it need to be universal" is not a valid reason, it's more the opposite ; the simpler is the code, the higher are the chance that it will works even with fucked up games, and both previous and future versions of Ren'Py.


Did you not consider that I will need to display other text in different size and in different container?
It's you who isn't considering that you don't need to do this. At least not so often and so blindly. Your own interface need constancy. Not only would it be less confusing for the user, would it simplify your life, but it would also not risk to overload the user computer, and have higher chances to be compatible with both the past and the future.
 

ZLZK

Member
Modder
Jul 2, 2017
276
626
No, it's solving what you where complaining about.
I wasn't complaining about anything I just stated what I have noticed.

And 12 of them are constants that don't need to be computed more than once ; at init time right before you define your styles, or at worse as config.start_callbacks callback.
As I have tried to say I will move them to styles.

I mean that each time I look at code you write, I have a headache due to its useless complexity.
You code the way you want, and design this code like you want it, but most of the time it could be way more simple. And, no, "it need to be universal" is not a valid reason, it's more the opposite ; the simpler is the code, the higher are the chance that it will works even with fucked up games, and both previous and future versions of Ren'Py.
It's simple to me, not so much your point of view.
I guess everything is simple to me as long as I understand it.

It's you who isn't considering that you don't need to do this. At least not so often and so blindly. Your own interface need constancy. Not only would it be less confusing for the user, would it simplify your life, but it would also not risk to overload the user computer, and have higher chances to be compatible with both the past and the future.
But I did it performance friendly though.
Since it's in default statement I think it runs only once when screen appear.
Then function does only once dict lookup.

And I prefer cutting string in function, not in screen.
I think both ways have similar performance.

From all of this I have learned that each screen has its own scope.
 

ZLZK

Member
Modder
Jul 2, 2017
276
626
I have decided to not use rendered text/character width.
It's overkill and as of now I need it only for 1 textbutton,
and I don't need translations for it.

Since approximate character width is half of its height. (Font size is height.)
I will use that as a reference.

Python:
x = int(round(float(box_width) / float(font_size))) - 1

text_limit = (x, x*2)

(...)

i, x = text_limit

if len(text) > x:
    text = text[:i] + "..." + text[-i:]
.
And since I'm displaying centered text,
truncating in the middle seems better than on the right side.