Skip to content

Develop #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 69 additions & 58 deletions Runtime/Scripts/BindingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using JetBrains.Annotations;
using System;
using System.Collections;
using System.Collections.Generic;
Expand All @@ -7,22 +8,52 @@

namespace RedMoon.ReactiveKit
{
public static class BindingExtensions
public static partial class BindingExtensions
{
#region Visual Element
public static IDisposable BindEnabled(this VisualElement element, IReadOnlyReactiveProperty<bool> property)
/// <summary>
/// Binds Element's Enabled State to Boolean Observable
/// </summary>
/// <param name="element">Element to Bind</param>
/// <param name="observable">Observable Stream to Subscribe to</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindEnabled(this VisualElement element, IObservable<bool> observable)
{
return observable.ObserveOnMainThread().DistinctUntilChanged().Subscribe(element.SetEnabled);
}
/// <summary>
/// Binds Element's Visibility State to Visibility Observable
/// </summary>
/// <param name="element">Element to Bind</param>
/// <param name="observable">Observable Stream to Subscribe to</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindVisibility(this VisualElement element, IObservable<Visibility> observable)
{
return property.SubscribeWithState(element, (__property, __element) =>
{
element.SetEnabled(__property);
});
return observable.ObserveOnMainThread().DistinctUntilChanged().Subscribe(value => element.style.visibility = value);
}
/// <summary>
/// Bind Element's Click Callback to Command
/// </summary>
/// <param name="element">Element to Bind</param>
/// <param name="command">Command to Execute</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindClick(this VisualElement element, IReactiveCommand<ClickEvent> command)
{
var d1 = element.BindEnabled(command.CanExecute);
var d2 = element.BindCallback(command);
IDisposable d1 = element.BindEnabled(command.CanExecute);
IDisposable d2 = element.BindCallback(command);
return StableCompositeDisposable.Create(d1, d2);
}
/// <summary>
/// Bind Element's Click Callback to Command
/// </summary>
/// <param name="element">Element to Bind</param>
/// <param name="command">Command to Execute</param>
/// <param name="dataForCallback">Callback as a part of Command</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindClick<TArgs>(this VisualElement element, IReactiveCommand<(ClickEvent, TArgs)> command, TArgs dataForCallback)
{
var d1 = element.BindEnabled(command.CanExecute);
Expand All @@ -32,97 +63,77 @@ public static IDisposable BindClick<TArgs>(this VisualElement element, IReactive
#endregion

#region Callback Event Handler
[MustUseReturnValue]
public static IDisposable BindCallback<TEventType>(this CallbackEventHandler element, EventCallback<TEventType> callback, TrickleDown trickleDown = TrickleDown.NoTrickleDown) where TEventType : EventBase<TEventType>, new()
{
element.RegisterCallback(callback, trickleDown);
return Disposable.Create(() => { element.UnregisterCallback(callback, trickleDown); });
}
[MustUseReturnValue]
public static IDisposable BindCallback<TEventType, TUserArgsType>(this CallbackEventHandler element, EventCallback<TEventType, TUserArgsType> callback, TUserArgsType dataForCallback, TrickleDown trickleDown = TrickleDown.NoTrickleDown) where TEventType : EventBase<TEventType>, new()
{
element.RegisterCallback(callback, dataForCallback, trickleDown);
return Disposable.Create(() => { element.UnregisterCallback(callback, trickleDown); });
}
[MustUseReturnValue]
public static IDisposable BindCallback<TEventType>(this CallbackEventHandler element, IReactiveCommand<TEventType> command, TrickleDown trickleDown = TrickleDown.NoTrickleDown) where TEventType : EventBase<TEventType>, new()
{
var callback = new EventCallback<TEventType>((ev) => command.Execute(ev));
EventCallback<TEventType> callback = new EventCallback<TEventType>((ev) => command.Execute(ev));
element.RegisterCallback(callback, trickleDown);
return Disposable.Create(() => { element.UnregisterCallback(callback, trickleDown); });
}
[MustUseReturnValue]
public static IDisposable BindCallback<TEventType, TUserArgsType>(this CallbackEventHandler element, IReactiveCommand<(TEventType, TUserArgsType)> command, TUserArgsType dataForCallback, TrickleDown trickleDown = TrickleDown.NoTrickleDown) where TEventType : EventBase<TEventType>, new()
{
var callback = new EventCallback<TEventType, TUserArgsType>((ev, args) => command.Execute((ev, args)));
EventCallback<TEventType, TUserArgsType> callback = new EventCallback<TEventType, TUserArgsType>((ev, args) => command.Execute((ev, args)));
element.RegisterCallback(callback, dataForCallback, trickleDown);
return Disposable.Create(() => { element.UnregisterCallback(callback, trickleDown); });
}
#endregion

#region Notifications
[MustUseReturnValue]
public static IDisposable BindValueChanged<T>(this INotifyValueChanged<T> element, IReactiveCommand<ChangeEvent<T>> command)
{
var callback = new EventCallback<ChangeEvent<T>>((ev) => command.Execute(ev));
EventCallback<ChangeEvent<T>> callback = new EventCallback<ChangeEvent<T>>((ev) => command.Execute(ev));
element.RegisterValueChangedCallback(callback);
return Disposable.Create(() => { element.UnregisterValueChangedCallback(callback); });
}
/// <summary>
/// Binds a Property Value to Element Changing
/// </summary>
/// <typeparam name="T">Type for Change Event</typeparam>
/// <param name="element">Element that Notifies Change</param>
/// <param name="property">Property to Change on Notification</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindValueChanged<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property)
{
var callback = new EventCallback<ChangeEvent<T>>((ev) => property.Value = ev.newValue);
EventCallback<ChangeEvent<T>> callback = new EventCallback<ChangeEvent<T>>((ev) => property.Value = ev.newValue);
element.RegisterValueChangedCallback(callback);
return Disposable.Create(() => { element.UnregisterValueChangedCallback(callback); });
}
/// <summary>
/// Binds Element changes to Update Property Values.
/// Then Initializes Values.
/// Binds an Element to a Property Changing
/// Does not Trigger Element Notifications
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="element">Element with Value Changed Callbacks</param>
/// <param name="property">Reactive Property that value should change on a Value Changed Callback</param>
/// <param name="stateIsProperty">Whether Default Value should be Default Property Value</param>
/// <returns></returns>
public static IDisposable BindValueChangedWithState<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property, bool stateIsProperty = true)
{
var d1 = BindValueChanged(element, property);
if (stateIsProperty)
{
element.value = property.Value;
}
else
{
property.Value = element.value;
}
return d1;
}
public static IDisposable BindToValueChanged<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property)
{
return property.SubscribeWithState(element, (__property, __element) =>
{
__element.SetValueWithoutNotify(__property);
});
}
public static IDisposable BindToValueChangedWithState<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property, bool stateIsProperty = true)
/// <typeparam name="T">Type for Fields</typeparam>
/// <param name="element">Element that has value updated</param>
/// <param name="observable">Observable to update value</param>
/// <returns>Disposable for Disposing Subscription</returns>
[MustUseReturnValue]
public static IDisposable BindToValueChanged<T>(this INotifyValueChanged<T> element, IObservable<T> observable)
{
var d1 = BindToValueChanged(element, property);
if (stateIsProperty)
{
element.value = property.Value;
}
else
{
property.Value = element.value;
}
return d1;
return observable.ObserveOnMainThread().DistinctUntilChanged().Subscribe((ev) => element.SetValueWithoutNotify(ev));
}
[MustUseReturnValue]
public static IDisposable BindTwoWayValueChanged<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property)
{
var d1 = BindValueChanged(element, property);
var d2 = BindToValueChanged(element, property);
return StableCompositeDisposable.Create(d1, d2);
}
public static IDisposable BindTwoWayValueChangedWithState<T>(this INotifyValueChanged<T> element, IReactiveProperty<T> property, bool stateIsProperty = true)
{
var d1 = BindValueChangedWithState(element, property, stateIsProperty);
var d2 = BindToValueChangedWithState(element, property, stateIsProperty);
IDisposable d1 = BindValueChanged(element, property);
IDisposable d2 = BindToValueChanged(element, property);
return StableCompositeDisposable.Create(d1, d2);
}
#endregion

}
}
9 changes: 9 additions & 0 deletions Runtime/Scripts/EventExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ public static class EventExtensions
{
return new EventCallbackObservable<TEventType>(element);
}

public static IDisposable SubscribeToExecuteCommand<T>(this IObservable<T> observable, IReactiveCommand<T> reactive)
{
return observable.Subscribe(x => reactive.Execute(x));
}
public static IDisposable SubscribeToUpdateProperty<T>(this IObservable<T> observable, IReactiveProperty<T> property)
{
return observable.Subscribe(x => property.Value = x);
}
}

internal class EventCallbackObservable<TEventType> : OperatorObservableBase<TEventType> where TEventType : EventBase<TEventType>, new()
Expand Down
72 changes: 63 additions & 9 deletions Samples/SamplesBasicUI/Scripts/MainView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,85 @@ public override void OnActivation(MainViewModel viewModel, CompositeDisposable d
Button button = Root.Q<Button>("ReactiveButton");
Label label = Root.Q<Label>("Output");
TextField field = Root.Q<TextField>("ReactiveTextField");
Slider slider = Root.Q<Slider>("ReactiveSlider");
MinMaxSlider minMaxSlider = Root.Q<MinMaxSlider>("ReactiveMinMaxSlider");
Toggle toggle = Root.Q<Toggle>("ReactiveToggle");

Label fieldLabel = field.labelElement;

//Bind such that whenever OnButtonClick can Execute, you can click button.
//Bind such that whenever Button is clicked, it executes OnButtonClick.
button.BindClick(viewModel.OnButtonClick).AddTo(disposable);
button
.BindClick(viewModel.OnButtonClick)
.AddTo(disposable);

//Turn Event into Observable
//Buffer based on Throttle Window
//Check that there is more than 2 clicks
//Double Click Detected
//Select Last ClickEvent for Processing
var clickStream = button.AsObservable<ClickEvent>();
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250))).Where(xs => xs.Count >= 2).Subscribe(x =>
{
viewModel.OnDoubleClick.Execute(x.Last());
});
clickStream
.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Select(x => x.Last())
.SubscribeToExecuteCommand(viewModel.OnDoubleClick)
.AddTo(disposable);

//Bind such that whenever DebugText changes, it updates label value.
label.BindToValueChanged(viewModel.DebugText).AddTo(disposable);

//Bind such that whenever field value changes, it executes OnTextFieldChange
//Bind such that whenever field value changes, it updates reactive text. Initializes to default field value.
field.BindValueChanged(viewModel.OnTextFieldChange).AddTo(disposable);
field.BindValueChangedWithState(viewModel.ReactiveText, false).AddTo(disposable);
field
.BindValueChanged(viewModel.OnTextFieldChange)
.AddTo(disposable);
field
.BindValueChanged(viewModel.ReactiveText)
.AddTo(disposable);
//Bind such that whenever ReactiveText changes, it updates fieldlabel value.
fieldLabel.BindToValueChanged(viewModel.ReactiveText).AddTo(disposable);
fieldLabel
.BindToValueChanged(viewModel.ReactiveText)
.AddTo(disposable);

//Observe the Slider Changing outside of Events
//Buffer based on Throttle Window
//Select Last
var sliderStream = slider.ObserveEveryValueChanged(x => x.value);
var fullStream = sliderStream
.Buffer(sliderStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Select(x => x.Last());
//Have Stream Update Max Value
fullStream
.SubscribeToUpdateProperty(viewModel.MaxValue)
.AddTo(disposable);
//Have Stream Execute Change Event
fullStream
.SubscribeToExecuteCommand(viewModel.OnSliderFieldChange)
.AddTo(disposable);

//Sometimes Binding to Certain Properties needs to be done manually.
//In this case lowLimit and highLimit are properties that do not inform
viewModel
.MinMaxValue
.Subscribe(x => UpdateMinMaxSlider(minMaxSlider, x))
.AddTo(disposable);

//Bind to Observe Values Changing
//Then Execute Commands
minMaxSlider
.ObserveEveryValueChanged(x => x.value)
.SubscribeToExecuteCommand(viewModel.OnMinMaxSliderFieldChange)
.AddTo(disposable);

toggle
.BindValueChanged(viewModel.OnToggleChange)
.AddTo(disposable);
}

private void UpdateMinMaxSlider(MinMaxSlider slider, Vector2 limit)
{
slider.lowLimit = limit.x;
slider.highLimit = limit.y;
}
}
}
30 changes: 24 additions & 6 deletions Samples/SamplesBasicUI/Scripts/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,50 @@ public class MainViewModel : ViewModel<MainViewModel>
{
public ReactiveProperty<string> DebugText { get; protected set; } = new ReactiveProperty<string>("");
public ReactiveProperty<string> ReactiveText { get; protected set; } = new ReactiveProperty<string>("");
public ReactiveProperty<float> MaxValue { get; protected set; } = new ReactiveProperty<float>();
public ReactiveProperty<float> MinValue { get; protected set; } = new ReactiveProperty<float>();
public IReadOnlyReactiveProperty<Vector2> MinMaxValue { get; protected set; }

public ReactiveCommand<ClickEvent> OnButtonClick;
public ReactiveCommand<ClickEvent> OnDoubleClick;
public ReactiveCommand<ChangeEvent<string>> OnTextFieldChange;
public ReactiveCommand<float> OnSliderFieldChange;
public ReactiveCommand<Vector2> OnMinMaxSliderFieldChange;
public ReactiveCommand<ChangeEvent<bool>> OnToggleChange;

public override void OnInitialization()
{
MinMaxValue = MinValue.CombineLatest(MaxValue, (x, y) => new Vector2(x, y)).ToReactiveProperty();

//Note: it is best practice to try for one line lambdas or to de-anonymize functions
//Unless of course, you want to make it difficult to inject code.
OnButtonClick = new ReactiveCommand<ClickEvent>();
OnButtonClick.Subscribe((c) => UpdateDebugText(c, "Button Clicked"));
OnButtonClick.Subscribe((c) => UpdateDebugText("Button Clicked"));

OnDoubleClick = new ReactiveCommand<ClickEvent>();
OnDoubleClick.Subscribe((c) => UpdateDebugText(c, "Double Clicked"));

OnDoubleClick.Subscribe((c) => UpdateDebugText("Double Clicked"));

OnTextFieldChange = new ReactiveCommand<ChangeEvent<string>>();
OnTextFieldChange.Subscribe((c) => UpdateDebugText(c, "Field Changed:"));

OnSliderFieldChange = new ReactiveCommand<float>();
OnSliderFieldChange.Subscribe((c) => UpdateDebugText($"Slider Changed: {c}"));

OnMinMaxSliderFieldChange = new ReactiveCommand<Vector2>();
OnMinMaxSliderFieldChange.Subscribe((c) => UpdateDebugText($"MinMaxChanged: ({c.x},{c.y})"));

OnToggleChange = new ReactiveCommand<ChangeEvent<bool>>();
OnToggleChange.Subscribe((c) => UpdateDebugText($"Toggle {c.newValue}"));
}

private void UpdateDebugText(ChangeEvent<string> obj, string Text)

private void UpdateDebugText<T>(ChangeEvent<T> obj, string Text)
{
DebugText.Value += $"{Text} {obj.previousValue} -> {obj.newValue}\n";
}

private void UpdateDebugText(ClickEvent obj, string Text)
private void UpdateDebugText(string Text)
{
Debug.Log(obj);
DebugText.Value += $"{Text}\n";
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"displayName": "Red Moon Reactive Kit",
"name": "io.crazyjackel.redmoon-reactivekit",
"unity": "2020.3",
"version": "1.0.6",
"version": "1.0.7",
"dependencies": {
"com.unity.ui": "1.0.0-preview.18"
},
Expand Down