Site Update in progress. Please excuse any potential mess. 😊
Menu

AutoText

https://answers.unity.com/questions/1865976/ui-toolkit-text-best-fit.html

import { Dom } from "OneJS/Dom"
import { h, render } from "preact"
import { useRef, useEffect } from "preact/hooks"
import { Mathf } from "UnityEngine"
import { Length, LengthUnit, MeshGenerationContext, StyleFloat, StyleLength, TextElement } from "UnityEngine/UIElements"
import { MeasureMode } from "UnityEngine/UIElements/VisualElement"

const AutoText = (props: { text?: string }) => {
    const ref = useRef<Dom>();
    let dirtyCount = 0;

    useEffect(() => {
        ref.current.ve.style.opacity = new StyleFloat(0);
        ref.current.ve.generateVisualContent = onGenerateVisualContent
        ref.current.ve.MarkDirtyRepaint()
    }, [])

    function onGenerateVisualContent(mgc: MeshGenerationContext) {
        dirtyCount++
        setTimeout(resize)
    }

    function resize() {
        let contentRect = ref.current.ve.contentRect
        let textElement = (ref.current.ve.Children() as any)[0] as TextElement

        let textSize = textElement.MeasureTextSize(props.text, 99999, MeasureMode.AtMost, 99999, MeasureMode.AtMost);
        let fontSize = Mathf.Max(ref.current.ve.style.fontSize.value.value, 1);
        let heightDictatedFontSize = Mathf.Abs(contentRect.height);
        let widthDictatedFontSize = Mathf.Abs(contentRect.width / textSize.x) * fontSize;
        let newFontSize = Mathf.FloorToInt(Mathf.Min(heightDictatedFontSize, widthDictatedFontSize));
        newFontSize = Mathf.Clamp(newFontSize, 1, 9999);

        ref.current.ve.style.fontSize = new StyleLength(new Length(newFontSize, LengthUnit.Pixel))
        if (dirtyCount > 1) {
            ref.current.ve.style.opacity = new StyleFloat(1);
        } else {
            ref.current.ve.MarkDirtyRepaint()
        }
    }

    return <div ref={ref} class="w-full h-full">{props.text}</div>
}

const App = () => {

    return <div name="app" class="w-full h-full">
        <AutoText text="Hey!!" />
    </div>
}

render(<App />, document.body)

The dirtyCount and opacity usage is to get rid of the 1-frame flicker that may happen during resizing. This is because both Preact and UI Toolkit re-rendering has a 1 frame delay.

Here's another example based on a textfield:

import { Dom } from "OneJS/Dom"
import { h, render } from "preact"
import { useEffect, useRef, useState } from "preact/hooks"
import { Mathf } from "UnityEngine";
import { StyleFloat, MeshGenerationContext, TextElement, StyleLength, Length, LengthUnit } from "UnityEngine/UIElements"
import { MeasureMode } from "UnityEngine/UIElements/VisualElement"

document.clearRuntimeStyleSheets()  // For reloading
document.addRuntimeUSS(`
    #unity-text-input {
        padding: 0;
        margin: 0;
    }
    #foo TextElement {
        -unity-text-align: middle-center;
        width: 100%;
    }
`)

const App = () => {
    const ref = useRef<Dom>()
    const [text, setText] = useState("Hello World")
    let dirtyCount = 0

    useEffect(() => {
        ref.current.ve.style.opacity = new StyleFloat(0)
        ref.current.ve.generateVisualContent = onGenerateVisualContent
        ref.current.ve.MarkDirtyRepaint()
    }, [])

    function onGenerateVisualContent(mgc: MeshGenerationContext) {
        dirtyCount++
        setTimeout(resize)
    }

    function resize() {
        let textElement = (ref.current.ve.Children() as any)[0][0] as TextElement
        let contentRect = textElement.contentRect

        let textSize = textElement.MeasureTextSize(text, 99999, MeasureMode.AtMost, 99999, MeasureMode.AtMost);
        let fontSize = Mathf.Max(ref.current.ve.style.fontSize.value.value, 1);
        let heightDictatedFontSize = Mathf.Abs(contentRect.height);
        let widthDictatedFontSize = Mathf.Abs(contentRect.width / textSize.x) * fontSize;
        let newFontSize = Mathf.FloorToInt(Mathf.Min(heightDictatedFontSize, widthDictatedFontSize));
        newFontSize = Mathf.Clamp(newFontSize, 1, 9999);

        ref.current.ve.style.fontSize = new StyleLength(new Length(newFontSize, LengthUnit.Pixel))
        if (dirtyCount > 1) {
            ref.current.ve.style.opacity = new StyleFloat(1);
        } else {
            ref.current.ve.MarkDirtyRepaint()
        }
    }

    return <div class="w-full h-full flex justify-center p-3">
        <div class="mx-auto bg-white overflow-hidden" style={{ width: "80%", height: "80%" }}>
            <div class="flex flex-col h-full">
                <div class="shrink-0 h-full" style={{ height: "20%" }}>
                    <textfield ref={ref} id="foo" class="w-full h-full" text={text} style={{ fontSize: "80%" }} />
                </div>
                <div class="p-8 bg-red-200" style={{ height: "80%" }}></div>
            </div>
        </div>
    </div>
}

render(<App />, document.body)