Search Unity

Feature Request ClickableElement: Like a button without text that can have children

Discussion in 'UI Toolkit' started by carlosfritz, Apr 10, 2020.

  1. carlosfritz

    carlosfritz

    Joined:
    Feb 17, 2018
    Posts:
    32
    The following works like a button:

    in_builder.png

    The structure looks like this:

    hierarchy.png

    A problem is, that PointerClickable is internal. So i had to copy/paste it, which maybe allowed for fooling around, but definitly not for using in a release. Same goes for the following:

    Code (CSharp):
    1. using System;
    2. using UnityEngine.UIElements;
    3.  
    4. namespace Jupiter.UIElements
    5. {
    6.   public class ClickableElement : BindableElement
    7.   {
    8.     public static readonly string ussClassName = "jupiter-clickable-element";
    9.  
    10.     public new class UxmlFactory : UxmlFactory<ClickableElement, UxmlTraits> { }
    11.     public new class UxmlTraits : BindableElement.UxmlTraits { }
    12.  
    13.     Clickable _clickable;
    14.  
    15.     public Clickable clickable
    16.     {
    17.       get
    18.       {
    19.         return _clickable;
    20.       }
    21.       set
    22.       {
    23.         if (_clickable != null && _clickable.target == this)
    24.         {
    25.           this.RemoveManipulator(_clickable);
    26.         }
    27.  
    28.         _clickable = value;
    29.  
    30.         if (_clickable != null)
    31.         {
    32.           this.AddManipulator(_clickable);
    33.         }
    34.       }
    35.     }
    36.  
    37.     public event Action clicked
    38.     {
    39.       add
    40.       {
    41.         if (_clickable == null)
    42.         {
    43.           clickable = new PointerClickable(value);
    44.         }
    45.         else
    46.         {
    47.           _clickable.clicked += value;
    48.         }
    49.       }
    50.       remove
    51.       {
    52.         if (_clickable != null)
    53.         {
    54.           _clickable.clicked -= value;
    55.         }
    56.       }
    57.     }
    58.  
    59.     public ClickableElement() : this(null)
    60.     {
    61.     }
    62.  
    63.     public ClickableElement(System.Action clickEvent)
    64.     {
    65.       AddToClassList(ussClassName);
    66.  
    67.       // Click-once behaviour
    68.       clickable = new PointerClickable(clickEvent);
    69.     }
    70.   }
    71. }
    Side note: In the game window it looks like that:

    in_game_window.png

    Haven't really tried to find out why...
     
  2. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,913
    You can just use a Button with no text.
     
  3. carlosfritz

    carlosfritz

    Joined:
    Feb 17, 2018
    Posts:
    32
    I tried, something funky happend, don't remember. I looked up the reference and it says "Permitted child elements: None". I checked the code of TextElement which is Button's base class:
    Code (CSharp):
    1. public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    2. {
    3.     get { yield break; }
    4. }
    According to the manual:
     
  4. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,913
    I forget what that means. It's still a VisualElement and you can give it children. I add labels/images and everything to some of my buttons.
     
  5. carlosfritz

    carlosfritz

    Joined:
    Feb 17, 2018
    Posts:
    32
    Even if it worked flawlessly, it would still be hackish. It should be the other way around: If you want a button with just text, then add just a Label to the ClickableElement.
     
  6. JoNax97

    JoNax97

    Joined:
    Feb 4, 2016
    Posts:
    611
    Yes, I agree that having text doesn't sound like the responsibility of a button. But at this point it probably would be too compatibility breaking to be changed.
     
  7. carlosfritz

    carlosfritz

    Joined:
    Feb 17, 2018
    Posts:
    32
    Maybe not. I derive ClickableElement from BindableElement. Button is derived from TextElement which is derived from BindableElement. So we only have to care for what TextElement brings to the table and map that from the new Button that would be derived from ClickableElement to a child Label which is derived from TextElement.

    According to the API, TextElement has one public property named "text" and one
    public method named "MeasureTextSize" and in the source code i found one protected internal method named "DoMeasure".
     
  8. carlosfritz

    carlosfritz

    Joined:
    Feb 17, 2018
    Posts:
    32
    A first draft:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UIElements;
    3.  
    4. namespace Jupiter.UIElements
    5. {
    6.   public class Button : ClickableElement
    7.   {
    8.     public new class UxmlFactory : UxmlFactory<Button, UxmlTraits> { }
    9.  
    10.     public new class UxmlTraits : ClickableElement.UxmlTraits
    11.     {
    12.       UxmlStringAttributeDescription m_Text = new UxmlStringAttributeDescription { name = "text", defaultValue = "Button" };
    13.  
    14.       public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    15.       {
    16.         base.Init(ve, bag, cc);
    17.         ((Button)ve).text = m_Text.GetValueFromBag(bag, cc);
    18.       }
    19.     }
    20.  
    21.     public string text
    22.     {
    23.       get
    24.       {
    25.         return label.text;
    26.       }
    27.       set
    28.       {
    29.         label.text = value;
    30.       }
    31.     }
    32.  
    33.     public Vector2 MeasureTextSize(string textToMeasure, float width, MeasureMode widthMode, float height, MeasureMode heightMode)
    34.     {
    35.       return label.MeasureTextSize(textToMeasure, width, widthMode, height, heightMode);
    36.     }
    37.  
    38.     public new static readonly string ussClassName = "unity-button";
    39.     public static readonly string labelUssClassName = ussClassName + "__label";
    40.  
    41.     public Label label { get; private set; }
    42.  
    43.     public Button() : this(null)
    44.     {
    45.     }
    46.  
    47.     public Button(System.Action clickEvent)
    48.     {
    49.       AddToClassList(ussClassName);
    50.  
    51.       label = new Label() { name = "unity-label" };
    52.       label.AddToClassList(labelUssClassName);
    53.       //label.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
    54.       //label.RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
    55.       //label.RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
    56.       hierarchy.Add(label);
    57.  
    58.       // Click-once behaviour
    59.       clickable = new PointerClickable(clickEvent);
    60.     }
    61.  
    62.     private static readonly string NonEmptyString = " ";
    63.  
    64.     /*protected internal override Vector2 DoMeasure(float desiredWidth, MeasureMode widthMode, float desiredHeight, MeasureMode heightMode)
    65.     {
    66.       var textToMeasure = text;
    67.       if (string.IsNullOrEmpty(textToMeasure))
    68.       {
    69.         textToMeasure = NonEmptyString;
    70.       }
    71.       return MeasureTextSize(textToMeasure, desiredWidth, widthMode, desiredHeight, heightMode);
    72.     }*/
    73.   }
    74. }
    Don't know what's up with that RegisterCallback and DoMeasure stuff, but...
    Code (CSharp):
    1. using System;
    2. using UnityEngine.UIElements;
    3.  
    4. namespace Jupiter.UIElements
    5. {
    6.   public class ClickableElement : BindableElement
    7.   {
    8.     public new class UxmlFactory : UxmlFactory<ClickableElement, UxmlTraits> { }
    9.  
    10.     public new class UxmlTraits : BindableElement.UxmlTraits { }
    11.  
    12.     public static readonly string ussClassName = "jupiter-clickable-element";
    13.  
    14.     private Clickable m_Clickable;
    15.  
    16.     public Clickable clickable
    17.     {
    18.       get
    19.       {
    20.         return m_Clickable;
    21.       }
    22.       set
    23.       {
    24.         if (m_Clickable != null && m_Clickable.target == this)
    25.         {
    26.           this.RemoveManipulator(m_Clickable);
    27.         }
    28.  
    29.         m_Clickable = value;
    30.  
    31.         if (m_Clickable != null)
    32.         {
    33.           this.AddManipulator(m_Clickable);
    34.         }
    35.       }
    36.     }
    37.  
    38.     [Obsolete("onClick is obsolete. Use clicked instead (UnityUpgradable) -> clicked", true)]
    39.     public event Action onClick
    40.     {
    41.       add
    42.       {
    43.         clicked += value;
    44.       }
    45.       remove
    46.       {
    47.         clicked -= value;
    48.       }
    49.     }
    50.  
    51.     public event Action clicked
    52.     {
    53.       add
    54.       {
    55.         if (m_Clickable == null)
    56.         {
    57.           clickable = new PointerClickable(value);
    58.         }
    59.         else
    60.         {
    61.           m_Clickable.clicked += value;
    62.         }
    63.       }
    64.       remove
    65.       {
    66.         if (m_Clickable != null)
    67.         {
    68.           m_Clickable.clicked -= value;
    69.         }
    70.       }
    71.     }
    72.  
    73.     public ClickableElement() : this(null)
    74.     {
    75.     }
    76.  
    77.     public ClickableElement(System.Action clickEvent)
    78.     {
    79.       AddToClassList(ussClassName);
    80.  
    81.       // Click-once behaviour
    82.       clickable = new PointerClickable(clickEvent);
    83.     }
    84.   }
    85. }
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace Jupiter.UIElements
    6. {
    7.   public class PointerClickable : Clickable
    8.   {
    9.     public PointerClickable(Action handler) : base(handler) { }
    10.     public PointerClickable(Action<EventBase> handler) : base(handler) { }
    11.     public PointerClickable(Action handler, long delay, long interval) : base(handler, delay, interval) { }
    12.  
    13.     public Vector2 lastPointerPosition
    14.     {
    15.       get { return lastMousePosition; }
    16.     }
    17.  
    18.     protected override void RegisterCallbacksOnTarget()
    19.     {
    20.       target.RegisterCallback<PointerDownEvent>(OnPointerDown);
    21.       target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
    22.       target.RegisterCallback<PointerUpEvent>(OnPointerUp);
    23.       base.RegisterCallbacksOnTarget();
    24.     }
    25.  
    26.     protected override void UnregisterCallbacksFromTarget()
    27.     {
    28.       target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
    29.       target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
    30.       target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
    31.       base.UnregisterCallbacksFromTarget();
    32.     }
    33.  
    34.     protected void OnPointerDown(PointerDownEvent evt)
    35.     {
    36.       if (!CanStartManipulation(evt)) return;
    37.  
    38.       if (evt.pointerId != PointerId.mousePointerId)
    39.       {
    40.         ProcessDownEvent(evt, evt.localPosition, evt.pointerId);
    41.         evt.PreventDefault();
    42.       }
    43.       else
    44.       {
    45.         evt.StopImmediatePropagation();
    46.       }
    47.     }
    48.  
    49.     protected void OnPointerMove(PointerMoveEvent evt)
    50.     {
    51.       if (evt.pointerId != PointerId.mousePointerId && active)
    52.       {
    53.         ProcessMoveEvent(evt, evt.localPosition);
    54.       }
    55.     }
    56.  
    57.     protected void OnPointerUp(PointerUpEvent evt)
    58.     {
    59.       if (evt.pointerId != PointerId.mousePointerId && active && CanStopManipulation(evt))
    60.       {
    61.         ProcessUpEvent(evt, evt.localPosition, evt.pointerId);
    62.       }
    63.     }
    64.   }
    65. }
    Try it! ;)
     
    Last edited: Apr 10, 2020
  9. flintmech

    flintmech

    Joined:
    Sep 29, 2011
    Posts:
    32
    How do you get the buttons to still work?

    I'm trying to add child elements to a Button, and whenever I do the Button stops being clickable. It just stops responding to my mouse at all.

    Searching for a solution to this led me to this thread from Google. The idea of having to create custom VisualElements for something that feels like such a basic need is really frustrating.