godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add Best Fit automatic font size option for Labels #7750

Open PickleJesus123 opened 1 year ago

PickleJesus123 commented 1 year ago

Describe the project you are working on

Porting my Unity games to Godot

Describe the problem or limitation you are having in your project

The games I am porting to Godot have localizations in many languages, which is problematic because strings can have a wildly different length depending the language that was selected. A common feature for text elements in Unity is an option called "Best Fit". When enabled, the font size of a Label dynamically shrinks/grows so that the text fills a rect/box perfectly.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Should be a simple checkbox on Label components that says 'Best Fit' or 'Resize text to Control Size'

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The code could be as simple as: "Does the text currently overflow the Label's bounding box? If yes, decrease font size repeatedly until it does not overflow.". This should also trigger if the text within a Label is changed during runtime

If this enhancement will not be used often, can it be worked around with a few lines of script?

This enhancement would be used often, as it is a commonly used feature in Unity

Is there a reason why this should be core and not an add-on in the asset library?

This is a commonly used feature in Unity, and arguably an industry standard

Calinou commented 1 year ago

This can be done with a script as shown here (with Label3D, but the same approach works for Label): https://github.com/godotengine/godot-demo-projects/blob/master/3d/labels_and_texts/label_3d_layout.gd

I have mixed opinions on whether this should be a core feature, as it can easily lead to unreadable text in certain languages due to the font size becoming very low. For accessibility reasons, resizing labels and wrapping them should always be considered first, and resizing text should only be a last resort.

If you look at UIs like Windows, macOS or Android, they pretty much never use automatic font sizing, and that's probably for a good reason.

Resizing the font only on one axis (i.e. making it more condensed) can help with accessibility somewhat, but it's not a silver bullet as text that is too condensed can be hard to read.

Lielay9 commented 1 year ago

This enhancement would be used often

An accessible design shouldn't lead to many labels that don't allow resizing or scrolling. The general recommendation is that the font should be resizable up to 2x of the original without breaking the layout:

https://learn.microsoft.com/en-us/gaming/accessibility/xbox-accessibility-guidelines/101#text-scaling.

If the target language is consistently different size or shape from the original, it can have its own Fonts, LabelSettings and Themes which can be configured with the resource remapping tool.

mikhaelmartin commented 10 months ago

I agree for localization, to make UI Container resized instead of font resize for accessibility reasons. But there are other cases that a text could be an "Object in Game World" instead of a "UI Component". I have a hard time scaling text in 2048 game. The script from @Calinou saved me. But it is easy to do on other game engine, I think it should be easy on Godot too.

2048-game

mrussogit commented 1 month ago

I came just to suggest this. I've read all perspectives on the subject I could find and it's reasonable that if a solution can be given on script, should not be part of the core. But let me add a different perspective:

First, Calinou is correct. There is no silver bullet. I've worked in several games in the past where we have a short button and then a German word come and mess everything. That's a work for translation to find an equivalent word that can be changed, make sense and isn't "size 3" after the auto-size do its job.

As far as I can tell, auto-size should be used within boundaries and needs to be verified. If we have a text that is a word or two, in a box that isn't expected to wrap, auto-size should work pretty well. If we have a text box that can have word/character variation, auto-size is also good, but the word/character variation needs to be checked to avoid the problem Calinou mentioned above, about too condensed and hard to read. Exception here is the great scroll in labels already native in Godot. It's another option.

If I use a script for label, like the one below:


extends Label

@export var autosize_on: bool = true
@export var use_default_font_size: bool = true
@export var font_size_override: int = -1
@export var font_size_min: int = 8
@export var font_size_max: int = 100
@export var font_size_width_percent: float = 0.9

var _current_font_size: int
var _default_font_size: int

func _ready():
    resized.connect(_on_resized)
    _initialize_default_font_size()
    _update_font_size()

func _initialize_default_font_size():
    _default_font_size = get_theme_font_size("font_size")
    if use_default_font_size:
        font_size_max = _default_font_size

func _on_resized():
    _update_font_size()

func _update_font_size():
    if not autosize_on:
        return

    if font_size_override >= 0:
        _current_font_size = font_size_override
    else:
        _current_font_size = _calculate_best_font_size()

    add_theme_font_size_override("font_size", _current_font_size)

func _calculate_best_font_size() -> int:
    var available_width = size.x * font_size_width_percent
    var available_height = size.y

    var font = get_theme_font("font")
    var font_size = font_size_max

    while font_size > font_size_min:
        var text_size = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
        if text_size.x <= available_width and text_size.y <= available_height:
            break

        font_size -= 1

    return int(max(font_size_min, min(font_size_max, font_size)))

the update font size needs to be called when a translation comes or when something changes the text. It's ok, better than having all text check in _process. But correct if I'm wrong. If a game is UI/label heavy, use a lot of title changes such as in a tooltip, UI, etc. One script for multiple labels would require this script to connect to a signal from a signal bus in autoload to have it refreshed, right? And then all UI updates at once? Assuming here it isn't worth to have separated signals for each case, nor separated scripts for each etc. Perhaps if this is integrated into the engine, the engine itself can optimize decide to update when a text changes. In addition to that this stuff can be totally toggled off by default and toggled on if needed. So no downside, no compatibility break (I guess?), and an additional option to reduce clutter in UI heavy games.