5
\$\begingroup\$

I'm creating a text box that allows for selectable text, based off of Unity's InputField. An issue I am now facing is that the selection highlight does not respond to masks (The parent has a mask, but it is just ignored) depth is also ignored, this seems to render through any other UI element that's in-front of it.:

enter image description here

I'm completely lost on how to go about masking it, so it does not show up past the bounds of the box. The code I used to generate it is heavily based off of Unity's, it's a bit long and kind of hacked together at the moment, I'm including it for references sake.

The method that generates the verts for the highlight in the .gif is GenerateHighlight

        private void UpdateGeometry()
    {
        if(caretCanvasRenderer == null && textBacker != null)
        {
            GameObject caret = new GameObject(transform.name + "Caret");
            caret.hideFlags = HideFlags.DontSave;
            caret.transform.SetParent(textBacker.transform);
            caret.transform.SetAsFirstSibling();
            caret.layer = gameObject.layer;

            caretRectTransform = caret.AddComponent<RectTransform>();
            caretCanvasRenderer = caret.AddComponent<CanvasRenderer>();
            caretCanvasRenderer.SetMaterial(Graphic.defaultGraphicMaterial, null);
            //caretCanvasRenderer.isMask = true;

            caret.AddComponent<LayoutElement>().ignoreLayout = true;

            AssignPositioningIfNeeded();
        }

        if (caretCanvasRenderer == null)
            return;

        OnFillVBO(VBO);

        if (VBO.Count == 0)
            caretCanvasRenderer.SetVertices(null, 0);
        else
            caretCanvasRenderer.SetVertices(VBO.ToArray(), VBO.Count);

        VBO.Clear();
    }

    private void OnFillVBO(List<UIVertex> vbo)
    {
        //if (!isFocused)
        //    return;

        Rect inputRect = textBacker.rectTransform.rect;
        Vector2 extents = inputRect.size;

        // get the text alignment anchor point for the text in local space
        Vector2 textAnchorPivot = Text.GetTextAnchorPivot(textBacker.alignment);
        Vector2 refPoint = Vector2.zero;
        refPoint.x = Mathf.Lerp(inputRect.xMin, inputRect.xMax, textAnchorPivot.x);
        refPoint.y = Mathf.Lerp(inputRect.yMin, inputRect.yMax, textAnchorPivot.y);

        // Ajust the anchor point in screen space
        Vector2 roundedRefPoint = textBacker.PixelAdjustPoint(refPoint);

        // Determine fraction of pixel to offset text mesh.
        // This is the rounding in screen space, plus the fraction of a pixel the text anchor pivot is from the corner of the text mesh.
        Vector2 roundingOffset = roundedRefPoint - refPoint + Vector2.Scale(extents, textAnchorPivot);
        roundingOffset.x = roundingOffset.x - Mathf.Floor(0.5f + roundingOffset.x);
        roundingOffset.y = roundingOffset.y - Mathf.Floor(0.5f + roundingOffset.y);

        if (!hasSelection)
            GenerateCursor(vbo, roundingOffset);
        else
            GenerateHightlight(vbo, roundingOffset);
    }

    private void GenerateCursor(List<UIVertex> vbo, Vector2 roundingOffset)
    {
        if (!caretVisible)
            return;
        if(cursorVerts == null)
            CreateCursorVerts();

        float width = 1f;
        float height = textBacker.fontSize;
        int adjustedPosition = Mathf.Max(0, caretPositionInternal - drawStart);
        TextGenerator generator = textBacker.cachedTextGenerator;

        if(generator == null)
            return;

        if (textBacker.resizeTextForBestFit)
            height = generator.fontSizeUsedForBestFit / textBacker.pixelsPerUnit;

        Vector2 startPosition = Vector2.zero;

        // Calculate startPosition
        if (generator.characterCountVisible + 1 > adjustedPosition || adjustedPosition == 0)
        {
            UICharInfo cursorChar = generator.characters[adjustedPosition];
            startPosition.x = cursorChar.cursorPos.x;
            startPosition.y = cursorChar.cursorPos.y;
        }
        startPosition.x /= textBacker.pixelsPerUnit;

        // TODO: Only clamp when Text uses horizontal word wrap.
        if (startPosition.x > textBacker.rectTransform.rect.xMax)
            startPosition.x = textBacker.rectTransform.rect.xMax;

        cursorVerts[0].position = new Vector3(startPosition.x, startPosition.y - height, 0.0f);
        cursorVerts[1].position = new Vector3(startPosition.x + width, startPosition.y - height, 0.0f);
        cursorVerts[2].position = new Vector3(startPosition.x + width, startPosition.y, 0.0f);
        cursorVerts[3].position = new Vector3(startPosition.x, startPosition.y, 0.0f);

        if (roundingOffset != Vector2.zero)
        {
            for (int i = 0; i < cursorVerts.Length; i++)
            {
                UIVertex uiv = cursorVerts[i];
                uiv.position.x += roundingOffset.x;
                uiv.position.y += roundingOffset.y;
                vbo.Add(uiv);
            }
        }
        else
        {
            for (int i = 0; i < cursorVerts.Length; i++)
            {
                vbo.Add(cursorVerts[i]);
            }
        }

        startPosition.y = Screen.height - startPosition.y;
        Input.compositionCursorPos = startPosition;
    }

    private void GenerateHightlight(List<UIVertex> vbo, Vector2 roundingOffset)
    {
        int startChar = Mathf.Max(0, caretPositionInternal - drawStart);
        int endChar = Mathf.Max(0, caretSelectPositionInternal - drawStart);

        // Ensure pos is always less then selPos to make the code simpler
        if (startChar > endChar)
        {
            int temp = startChar;
            startChar = endChar;
            endChar = temp;
        }

        endChar -= 1;
        TextGenerator generator = textBacker.cachedTextGenerator;

        int currentLineIndex = DetermineCharacterLine(startChar, generator);
        float height = textBacker.fontSize;

        if (textBacker.resizeTextForBestFit)
            height = generator.fontSizeUsedForBestFit / textBacker.pixelsPerUnit;

        if (generator != null && generator.lines.Count > 0)
        {
            // TODO: deal with multiple lines with different line heights.
            height = generator.lines[0].height;
        }

        if (textBacker.resizeTextForBestFit && generator != null)
        {
            height = generator.fontSizeUsedForBestFit;
        }

        int nextLineStartIdx = GetLineEndPosition(generator, currentLineIndex);

        UIVertex vert = UIVertex.simpleVert;
        vert.uv0 = Vector2.zero;
        vert.color = selectionColor;

        int currentChar = startChar;
        while (currentChar <= endChar && currentChar < generator.characterCountVisible)
        {
            if (currentChar + 1 == nextLineStartIdx || currentChar == endChar)
            {
                UICharInfo startCharInfo = generator.characters[startChar];
                UICharInfo endCharInfo = generator.characters[currentChar];
                Vector2 startPosition = new Vector2(startCharInfo.cursorPos.x / textBacker.pixelsPerUnit, startCharInfo.cursorPos.y);
                Vector2 endPosition = new Vector2((endCharInfo.cursorPos.x + endCharInfo.charWidth) / textBacker.pixelsPerUnit, startPosition.y - height / textBacker.pixelsPerUnit);

                // Checking xMin as well due to text generator not setting possition if char is not rendered.
                if (endPosition.x > textBacker.rectTransform.rect.xMax || endPosition.x < textBacker.rectTransform.rect.xMin)
                    endPosition.x = textBacker.rectTransform.rect.xMax;

                vert.position = new Vector3(startPosition.x, endPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.Add(vert);

                vert.position = new Vector3(endPosition.x, endPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.Add(vert);

                vert.position = new Vector3(endPosition.x, startPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.Add(vert);

                vert.position = new Vector3(startPosition.x, startPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.Add(vert);

                startChar = currentChar + 1;
                currentLineIndex++;

                nextLineStartIdx = GetLineEndPosition(generator, currentLineIndex);
            }
            currentChar++;
        }
    }

The gist of it, is it creates a new gameobject. Adds a canvas renderer, and rect transform. Calls the appropriate method that generates the vertices, and then assigns those to the `CanvasRenderer.

\$\endgroup\$
  • \$\begingroup\$ Had the exact same problem trying to implement selection based on the TextMeshPro Label :). Solution mentioned below worked for me. \$\endgroup\$ – Pking Jul 27 at 16:23
2
\$\begingroup\$

Take a look at UI.Graphic and UI.MaskableGraphic

You'll want to write a class that derives from [MaskableGraphic] then implement the methods to fill the VBO when the UI asks for it.

\$\endgroup\$
1
\$\begingroup\$

Add this to the new Input Caret GameObject created by the InputField and masking should work.

class Masker : MaskableGraphic
{
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.