-
-
Save scho/6ec3f30653119b3856b1a9edfd5b0b7b to your computer and use it in GitHub Desktop.
UI Toolkit Reactive Binding Extensions using UniRx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
using JetBrains.Annotations; | |
using ProjectHive.Core.Util.UniRx; | |
using UniRx; | |
using UnityEngine; | |
using UnityEngine.UIElements; | |
using UnityEngine.Video; | |
namespace ProjectHive.UI.Util.UiToolkit.Extensions | |
{ | |
/** | |
* This is a collection of binding extensions for UI Toolkit using UniRx | |
* | |
* The SmartObserveOnMainThread() is similar to ObserveOnMainThread(), except that it checks, if the current | |
* thread is already the main thread and if so, it will execute on it. This has the advantage, that changes to the | |
* UI originating from user input/the main thread won't have any delay. If all changes originate from the main | |
* thread anyways, it can safely be removed. | |
*/ | |
public static class UiToolkitBindingExtensions | |
{ | |
[MustUseReturnValue] | |
public static IDisposable BindEnabled(this VisualElement element, IObservable<bool> observable) | |
{ | |
return observable.SmartObserveOnMainThread().DistinctUntilChanged().Subscribe(element.SetEnabled); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindVisibility(this VisualElement element, IObservable<bool> observable) | |
{ | |
return observable.SmartObserveOnMainThread().DistinctUntilChanged().Subscribe(element.SetVisibility); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindWidth(this VisualElement element, IObservable<StyleLength> width) | |
{ | |
return width.SmartObserveOnMainThread().Subscribe(value => element.style.width = value); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindDisplay(this VisualElement visualElement, IObservable<bool> flex) | |
{ | |
return flex.SmartObserveOnMainThread() | |
.DistinctUntilChanged() | |
.Subscribe(visualElement.SetDisplay); | |
} | |
[MustUseReturnValue] | |
public static IDisposable Bind(this TextElement textElement, IObservable<string> observable) | |
{ | |
return observable.SmartObserveOnMainThread().Subscribe(value => textElement.text = value); | |
} | |
[MustUseReturnValue] | |
public static IDisposable Bind(this Button button, Action action) | |
{ | |
return button.ClickedAsObservable().Subscribe(_ => | |
{ | |
if (button.enabledSelf) | |
{ | |
action(); | |
} | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable Bind(this Button button, Func<Task> asyncAction) | |
{ | |
return button.ClickedAsObservable().Subscribe(_ => | |
{ | |
if (button.enabledSelf) | |
{ | |
MainThreadDispatcher.StartCoroutine(RunAsync(asyncAction)); | |
} | |
}); | |
} | |
private static IEnumerator RunAsync(Func<Task> asyncAction) | |
{ | |
var task = asyncAction(); | |
yield return new WaitUntil(() => task.IsCompleted); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindMouseDown(this Button button, Action action) | |
{ | |
return button.MouseDownEventAsObservable().Subscribe(_ => | |
{ | |
if (button.enabledSelf) | |
{ | |
action(); | |
} | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindHoldDown(this Button button, Action action) | |
{ | |
return Observable | |
.FromEvent(h => button.clickable = new PointerClickable(h, 0, 1), h => button.clicked -= h) | |
.Subscribe(_ => | |
{ | |
if (button.enabledSelf) | |
{ | |
action(); | |
} | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindLeftClick(this VisualElement visualElement, Action action) | |
{ | |
return visualElement.AsObservable<ClickEvent>().Where(evt => evt.button == 0).Subscribe(_ => action()); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindRightClick(this VisualElement visualElement, Action action) | |
{ | |
return new CompositeDisposable | |
{ | |
// For some reason, the ClickEvent does not work here, so the MouseUpEvent is used | |
visualElement.AsObservable<MouseUpEvent>().Where(evt => evt.button == 1) | |
.Subscribe(_ => action()), | |
visualElement.DisableRightClickForInputManager() | |
}; | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindClass(this VisualElement visualElement, | |
IObservable<Tuple<string, bool>> className) | |
{ | |
return className.SmartObserveOnMainThread() | |
.Subscribe(value => visualElement.EnableInClassList(value.Item1, value.Item2)); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindClass(this VisualElement visualElement, | |
IObservable<string> className, Predicate<string> predicateToRemove) | |
{ | |
return className.SmartObserveOnMainThread() | |
.Subscribe(value => | |
{ | |
visualElement.RemoveFromClassList(predicateToRemove); | |
visualElement.AddToClassList(value); | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindClasses(this VisualElement visualElement, | |
IObservable<IEnumerable<string>> classNames) | |
{ | |
return classNames.SmartObserveOnMainThread() | |
.Subscribe(value => | |
{ | |
visualElement.ClearClassList(); | |
foreach (var className in value) | |
{ | |
visualElement.AddToClassList(className); | |
} | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindStyle<T>(this VisualElement visualElement, IObservable<T> styleValue, | |
Action<VisualElement, T> styleAction) | |
{ | |
return styleValue.SmartObserveOnMainThread() | |
.Subscribe(value => styleAction(visualElement, value)); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindTopInPercent(this VisualElement visualElement, IObservable<float> top) | |
{ | |
return visualElement.BindStyle(top, (element, value) => | |
element.style.top = new StyleLength(new Length(value * 100, LengthUnit.Percent))); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindLeftInPercent(this VisualElement visualElement, IObservable<float> left) | |
{ | |
return visualElement.BindStyle(left, (element, value) => | |
element.style.left = new StyleLength(new Length(value * 100, LengthUnit.Percent))); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindRotation(this VisualElement visualElement, | |
IObservable<float> angleInDegree) | |
{ | |
return angleInDegree.SmartObserveOnMainThread() | |
.Subscribe(visualElement.SetRotation); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindClasses(this VisualElement visualElement, | |
IObservable<IEnumerable<Tuple<string, bool>>> classNames) | |
{ | |
return classNames.SmartObserveOnMainThread() | |
.Subscribe(values => | |
{ | |
foreach (var (className, addOrRemove) in values) | |
{ | |
visualElement.EnableInClassList(className, addOrRemove); | |
} | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable BindHorizontalScrolling(this Button button, ScrollView scrollView, | |
float scrollOffset) | |
{ | |
return button.Bind(() => | |
{ | |
var scroller = scrollView.horizontalScroller; | |
var value = Mathf.Clamp(scroller.value + scrollOffset, scroller.lowValue, scroller.highValue); | |
scrollView.scrollOffset = new Vector2(value, scrollView.scrollOffset.y); | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable Bind(this VideoPlayer player, VideoClip clip) | |
{ | |
player.clip = clip; | |
player.Play(); | |
return new ActionDisposable(() => | |
{ | |
player.Stop(); | |
player.clip = null; | |
player.targetTexture.Release(); | |
}); | |
} | |
[MustUseReturnValue] | |
public static IDisposable DisableRightClickForInputManager(this VisualElement element) | |
{ | |
return element.OnMouseOverAsObservable().Subscribe(value => InputManager.RightClickDisabled = value); | |
} | |
public static IObservable<bool> OnMouseOverAsObservable(this VisualElement element) | |
{ | |
return element.AsObservable<MouseOverEvent>().Select(evt => true) | |
.Merge(element.AsObservable<MouseOutEvent>().Select(evt => false)) | |
.Merge(element.AsObservable<DetachFromPanelEvent>().Select(evt => false)); | |
} | |
private static IObservable<TEventType> AsObservable<TEventType>(this CallbackEventHandler handler) | |
where TEventType : EventBase<TEventType>, new() | |
{ | |
return Observable.FromEvent<EventCallback<TEventType>, TEventType>(h => evt => h(evt), | |
h => handler.RegisterCallback(h), | |
h => handler.UnregisterCallback(h)); | |
} | |
private static IObservable<Unit> MouseDownEventAsObservable(this Button button) | |
{ | |
button.clickable.activators.Clear(); | |
return button.AsObservable<MouseDownEvent>().Where(evt => evt.button == 0).Select(_ => Unit.Default); | |
} | |
private static IObservable<Unit> ClickedAsObservable(this Button button) | |
{ | |
return Observable.FromEvent<Action, Unit>(h => () => h(Unit.Default), | |
h => button.clicked += h, | |
h => button.clicked -= h); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using UnityEngine; | |
using UnityEngine.UIElements; | |
namespace ProjectHive.UI.Util.UiToolkit.Extensions | |
{ | |
public static class UiToolkitExtensions | |
{ | |
public static void RemoveFromClassList(this VisualElement visualElement, Predicate<string> predicate) | |
{ | |
foreach (var className in visualElement.GetClasses() | |
.Where(className => predicate(className)).ToArray()) | |
{ | |
visualElement.RemoveFromClassList(className); | |
} | |
} | |
public static void EnableInClassList(this VisualElement visualElement, IEnumerable<Tuple<string, bool>> values) | |
{ | |
foreach (var (className, enable) in values) | |
{ | |
visualElement.EnableInClassList(className, enable); | |
} | |
} | |
public static void SetDisplay(this VisualElement visualElement, bool flex) | |
{ | |
visualElement.style.display = flex ? DisplayStyle.Flex : DisplayStyle.None; | |
} | |
public static void SetVisibility(this VisualElement visualElement, bool visible) | |
{ | |
visualElement.style.visibility = visible ? Visibility.Visible : Visibility.Hidden; | |
} | |
public static void AddToClassList(this VisualElement visualElement, IEnumerable<string> classNames) | |
{ | |
foreach (var className in classNames) | |
{ | |
visualElement.AddToClassList(className); | |
} | |
} | |
public static void SetRotation(this VisualElement visualElement, | |
float angleInDegree) | |
{ | |
visualElement.transform.rotation = Quaternion.AngleAxis(angleInDegree, Vector3.forward); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment