add reactive binding

This commit is contained in:
Matt Parker
2025-09-12 18:04:20 +10:00
parent 4485b51b1e
commit 1b2ebcb1e8
41 changed files with 1411 additions and 4 deletions

View File

@@ -1,13 +1,25 @@
using Godot;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Problems;
namespace SharpIDE.Godot.Features.BottomPanel;
public partial class BottomPanelManager : Panel
{
public SharpIdeSolutionModel? Solution
{
get;
set
{
field = value;
_problemsPanel.Solution = value;
}
}
private Control _runPanel = null!;
private Control _debugPanel = null!;
private Control _buildPanel = null!;
private Control _problemsPanel = null!;
private ProblemsPanel _problemsPanel = null!;
private Dictionary<BottomPanelType, Control> _panelTypeMap = [];
@@ -16,7 +28,7 @@ public partial class BottomPanelManager : Panel
_runPanel = GetNode<Control>("%RunPanel");
_debugPanel = GetNode<Control>("%DebugPanel");
_buildPanel = GetNode<Control>("%BuildPanel");
_problemsPanel = GetNode<Control>("%ProblemsPanel");
_problemsPanel = GetNode<ProblemsPanel>("%ProblemsPanel");
_panelTypeMap = new Dictionary<BottomPanelType, Control>
{

View File

@@ -0,0 +1,8 @@
using Godot;
namespace SharpIDE.Godot.Features.Problems;
public partial class ProblemEntry : Control
{
}

View File

@@ -0,0 +1 @@
uid://dkgiknux4rqit

View File

@@ -0,0 +1,30 @@
using System.Collections.Specialized;
using Godot;
using ObservableCollections;
using R3;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Godot.Features.Problems;
public partial class ProblemsPanel : Control
{
private PackedScene _problemsPanelProjectEntryScene = GD.Load<PackedScene>("uid://do72lghjvfdp3");
private VBoxContainer _vBoxContainer = null!;
// TODO: Use observable collections in the solution model and downwards
public SharpIdeSolutionModel? Solution { get; set; }
private readonly ObservableHashSet<SharpIdeProjectModel> _projects = [];
public override void _Ready()
{
Observable.EveryValueChanged(this, manager => manager.Solution)
.Where(s => s is not null)
.Subscribe(s =>
{
GD.Print($"ProblemsPanel: Solution changed to {s?.Name ?? "null"}");
_projects.Clear();
_projects.AddRange(s!.AllProjects);
});
_vBoxContainer = GetNode<VBoxContainer>("ScrollContainer/VBoxContainer");
_vBoxContainer.BindChildren(_projects, _problemsPanelProjectEntryScene);
}
}

View File

@@ -0,0 +1 @@
uid://b1r3no4u3khik

View File

@@ -1,4 +1,7 @@
[gd_scene format=3 uid="uid://tqpmww430cor"]
[gd_scene load_steps=3 format=3 uid="uid://tqpmww430cor"]
[ext_resource type="Script" uid="uid://b1r3no4u3khik" path="res://Features/Problems/ProblemsPanel.cs" id="1_bnenc"]
[ext_resource type="PackedScene" uid="uid://do72lghjvfdp3" path="res://Features/Problems/ProblemsPanelProjectEntry.tscn" id="2_xj8le"]
[node name="ProblemsPanel" type="Control"]
layout_mode = 3
@@ -7,3 +10,23 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_bnenc")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ProblemsPanelProjectEntry" parent="ScrollContainer/VBoxContainer" instance=ExtResource("2_xj8le")]
layout_mode = 2
[node name="ProblemsPanelProjectEntry2" parent="ScrollContainer/VBoxContainer" instance=ExtResource("2_xj8le")]
layout_mode = 2

View File

@@ -0,0 +1,14 @@
using Godot;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Godot.Features.Problems;
public partial class ProblemsPanelProjectEntry : MarginContainer
{
public SharpIdeProjectModel Project { get; set; } = null!;
public override void _Ready()
{
GetNode<Label>("Label").Text = Project?.Name ?? "NULL";
}
}

View File

@@ -0,0 +1 @@
uid://bx5v8rr87343o

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3 uid="uid://do72lghjvfdp3"]
[ext_resource type="Script" uid="uid://bx5v8rr87343o" path="res://Features/Problems/ProblemsPanelProjectEntry.cs" id="1_2yh8u"]
[node name="ProblemsPanelProjectEntry" type="MarginContainer"]
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_2yh8u")
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Project"

View File

@@ -2,6 +2,7 @@ using Godot;
using Microsoft.Build.Locator;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.BottomPanel;
using SharpIDE.Godot.Features.CustomControls;
using SharpIDE.Godot.Features.Run;
using SharpIDE.Godot.Features.SolutionExplorer;
@@ -19,6 +20,7 @@ public partial class IdeRoot : Control
private RunPanel _runPanel = null!;
private Button _runMenuButton = null!;
private Popup _runMenuPopup = null!;
private BottomPanelManager _bottomPanelManager = null!;
private readonly PackedScene _runMenuItemScene = ResourceLoader.Load<PackedScene>("res://Features/Run/RunMenuItem.tscn");
public override void _Ready()
@@ -34,6 +36,7 @@ public partial class IdeRoot : Control
_solutionExplorerPanel = GetNode<SolutionExplorerPanel>("%SolutionExplorerPanel");
_runPanel = GetNode<RunPanel>("%RunPanel");
_invertedVSplitContainer = GetNode<InvertedVSplitContainer>("%InvertedVSplitContainer");
_bottomPanelManager = GetNode<BottomPanelManager>("%BottomPanel");
_runMenuButton.Pressed += OnRunMenuButtonPressed;
_solutionExplorerPanel.FileSelected += OnSolutionExplorerPanelOnFileSelected;
@@ -71,6 +74,7 @@ public partial class IdeRoot : Control
var solutionModel = await VsPersistenceMapper.GetSolutionModel(path);
_solutionExplorerPanel.SolutionModel = solutionModel;
_sharpIdeCodeEdit.Solution = solutionModel;
_bottomPanelManager.Solution = solutionModel;
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred();
RoslynAnalysis.StartSolutionAnalysis(path);

View File

@@ -153,6 +153,7 @@ item_0/text = "Getting Context Actions..."
item_0/id = 0
[node name="BottomPanel" type="Panel" parent="VBoxContainer/HBoxContainer/InvertedVSplitContainer"]
unique_name_in_owner = true
layout_mode = 2
script = ExtResource("7_i62lx")

View File

@@ -1,7 +1,37 @@
using Godot;
using System.Collections.Specialized;
using Godot;
using ObservableCollections;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Problems;
namespace SharpIDE.Godot;
public static class ControlExtensions
{
extension(Control control)
{
public void BindChildren(ObservableHashSet<SharpIdeProjectModel> list, PackedScene scene)
{
var view = list.CreateView(x =>
{
var node = scene.Instantiate<ProblemsPanelProjectEntry>();
node.Project = x;
Callable.From(() => control.AddChild(node)).CallDeferred();
return node;
});
view.ViewChanged += OnViewChanged;
}
private static void OnViewChanged(in SynchronizedViewChangedEventArgs<SharpIdeProjectModel, ProblemsPanelProjectEntry> eventArgs)
{
GD.Print("View changed: " + eventArgs.Action);
if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
{
eventArgs.OldItem.View.QueueFree();
}
}
}
}
public static class NodeExtensions
{
extension(Node node)

View File

@@ -8,4 +8,8 @@
<ItemGroup>
<ProjectReference Include="..\SharpIDE.Application\SharpIDE.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ObservableCollections" Version="3.3.4" />
<PackageReference Include="R3" Version="1.3.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
#nullable enable
using System.Runtime.CompilerServices;
namespace R3;
public partial class FrameProviderDispatcher : global::Godot.Node
{
StrongBox<double> processDelta = new StrongBox<double>();
StrongBox<double> physicsProcessDelta = new StrongBox<double>();
public override void _Ready()
{
GodotProviderInitializer.SetDefaultObservableSystem();
((GodotFrameProvider)GodotFrameProvider.Process).Delta = processDelta;
((GodotFrameProvider)GodotFrameProvider.PhysicsProcess).Delta = physicsProcessDelta;
}
public override void _Process(double delta)
{
processDelta.Value = delta;
((GodotTimeProvider)GodotTimeProvider.Process).time += delta;
((GodotFrameProvider)GodotFrameProvider.Process).Run(delta);
}
public override void _PhysicsProcess(double delta)
{
physicsProcessDelta.Value = delta;
((GodotTimeProvider)GodotTimeProvider.PhysicsProcess).time += delta;
((GodotFrameProvider)GodotFrameProvider.PhysicsProcess).Run(delta);
}
}

View File

@@ -0,0 +1 @@
uid://ngclnv5y220f

View File

@@ -0,0 +1,80 @@
#nullable enable
using Godot;
using R3.Collections;
using System;
using System.Runtime.CompilerServices;
namespace R3;
internal enum PlayerLoopTiming
{
Process,
PhysicsProcess
}
public class GodotFrameProvider : FrameProvider
{
public static readonly GodotFrameProvider Process = new GodotFrameProvider(PlayerLoopTiming.Process);
public static readonly GodotFrameProvider PhysicsProcess = new GodotFrameProvider(PlayerLoopTiming.PhysicsProcess);
FreeListCore<IFrameRunnerWorkItem> list;
readonly object gate = new object();
PlayerLoopTiming PlayerLoopTiming { get; }
internal StrongBox<double> Delta = default!; // set from Node before running process.
internal GodotFrameProvider(PlayerLoopTiming playerLoopTiming)
{
this.PlayerLoopTiming = playerLoopTiming;
this.list = new FreeListCore<IFrameRunnerWorkItem>(gate);
}
public override long GetFrameCount()
{
if (PlayerLoopTiming == PlayerLoopTiming.Process)
{
return (long)Engine.GetProcessFrames();
}
else
{
return (long)Engine.GetPhysicsFrames();
}
}
public override void Register(IFrameRunnerWorkItem callback)
{
list.Add(callback, out _);
}
internal void Run(double _)
{
long frameCount = GetFrameCount();
var span = list.AsSpan();
for (int i = 0; i < span.Length; i++)
{
ref readonly var item = ref span[i];
if (item != null)
{
try
{
if (!item.MoveNext(frameCount))
{
list.Remove(i);
}
}
catch (Exception ex)
{
list.Remove(i);
try
{
ObservableSystem.GetUnhandledExceptionHandler().Invoke(ex);
}
catch { }
}
}
}
}
}

View File

@@ -0,0 +1 @@
uid://bus13cr6ba71l

View File

@@ -0,0 +1,32 @@
using System;
using Godot;
namespace R3;
public static class GodotNodeExtensions
{
/// <summary>
/// Dispose self on target node has bee tree exited.
/// </summary>
/// <param name="disposable"></param>
/// <param name="node"></param>
/// <typeparam name="T"></typeparam>
/// <returns>Self disposable</returns>
public static T AddTo<T>(this T disposable, Node node) where T : IDisposable
{
// Note: Dispose when tree exited, so if node is not inside tree, dispose immediately.
if (!node.IsInsideTree())
{
if (!node.IsNodeReady()) // Before enter tree
{
GD.PrintErr("AddTo does not support to use before enter tree.");
}
disposable.Dispose();
return disposable;
}
node.TreeExited += () => disposable.Dispose();
return disposable;
}
}

View File

@@ -0,0 +1 @@
uid://cob4ct214hg4v

View File

@@ -0,0 +1,78 @@
using System;
namespace R3;
public static class GodotObservableExtensions
{
public static Observable<double> Delta(this Observable<Unit> source)
{
return Delta(source, GodotFrameProvider.Process);
}
public static Observable<double> Delta(this Observable<Unit> source, GodotFrameProvider frameProvider)
{
return new Delta(source, frameProvider);
}
public static Observable<(double Delta, T Item)> Delta<T>(this Observable<T> source)
{
return Delta(source, GodotFrameProvider.Process);
}
public static Observable<(double Delta, T Item)> Delta<T>(this Observable<T> source, GodotFrameProvider frameProvider)
{
return new Delta<T>(source, frameProvider);
}
}
internal sealed class Delta(Observable<Unit> source, GodotFrameProvider frameProvider) : Observable<double>
{
protected override IDisposable SubscribeCore(Observer<double> observer)
{
return source.Subscribe(new _Delta(observer, frameProvider));
}
sealed class _Delta(Observer<double> observer, GodotFrameProvider frameProvider) : Observer<Unit>
{
protected override void OnNextCore(Unit value)
{
observer.OnNext(frameProvider.Delta.Value);
}
protected override void OnErrorResumeCore(Exception error)
{
observer.OnErrorResume(error);
}
protected override void OnCompletedCore(Result result)
{
observer.OnCompleted(result);
}
}
}
internal sealed class Delta<T>(Observable<T> source, GodotFrameProvider frameProvider) : Observable<(double Delta, T Item)>
{
protected override IDisposable SubscribeCore(Observer<(double Delta, T Item)> observer)
{
return source.Subscribe(new _Delta(observer, frameProvider));
}
sealed class _Delta(Observer<(double Delta, T Item)> observer, GodotFrameProvider frameProvider) : Observer<T>
{
protected override void OnNextCore(T value)
{
observer.OnNext((frameProvider.Delta.Value, value));
}
protected override void OnErrorResumeCore(Exception error)
{
observer.OnErrorResume(error);
}
protected override void OnCompletedCore(Result result)
{
observer.OnCompleted(result);
}
}
}

View File

@@ -0,0 +1 @@
uid://8lnifa1x4o6e

View File

@@ -0,0 +1,21 @@
#nullable enable
using Godot;
using System;
namespace R3;
public static class GodotProviderInitializer
{
public static void SetDefaultObservableSystem()
{
SetDefaultObservableSystem(ex => GD.PrintErr(ex));
}
public static void SetDefaultObservableSystem(Action<Exception> unhandledExceptionHandler)
{
ObservableSystem.RegisterUnhandledExceptionHandler(unhandledExceptionHandler);
ObservableSystem.DefaultTimeProvider = GodotTimeProvider.Process;
ObservableSystem.DefaultFrameProvider = GodotFrameProvider.Process;
}
}

View File

@@ -0,0 +1 @@
uid://cfm1x8aucvw47

View File

@@ -0,0 +1,32 @@
#if TOOLS
#nullable enable
using Godot;
namespace R3;
[Tool]
public partial class GodotR3Plugin : EditorPlugin
{
static ObservableTrackerDebuggerPlugin? observableTrackerDebugger;
public override void _EnterTree()
{
observableTrackerDebugger ??= new ObservableTrackerDebuggerPlugin();
AddDebuggerPlugin(observableTrackerDebugger);
// Automatically install autoloads here for ease of use.
AddAutoloadSingleton(nameof(FrameProviderDispatcher), "res://addons/R3.Godot/FrameProviderDispatcher.cs");
AddAutoloadSingleton(nameof(ObservableTrackerRuntimeHook), "res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs");
}
public override void _ExitTree()
{
if (observableTrackerDebugger != null)
{
RemoveDebuggerPlugin(observableTrackerDebugger);
observableTrackerDebugger = null;
}
RemoveAutoloadSingleton(nameof(FrameProviderDispatcher));
RemoveAutoloadSingleton(nameof(ObservableTrackerRuntimeHook));
}
}
#endif

View File

@@ -0,0 +1 @@
uid://cdglfkymcq38j

View File

@@ -0,0 +1,261 @@
using System;
using System.Threading;
using Godot;
namespace R3;
public static class GodotObjectExtensions
{
/// <summary>Returns a <see cref="CancellationToken"/> that cancels when <paramref name="obj"/> emits the specified signal, this cancellation is one-shot unless <paramref name="oneShot"/> is false.</summary>
public static CancellationToken CancelOnSignal(this GodotObject obj, StringName signalName, bool oneShot = true)
{
CancellationTokenSource cts = new();
obj.Connect(signalName, Callable.From(cts.Cancel), oneShot ? (uint) GodotObject.ConnectFlags.OneShot : 0);
return cts.Token;
}
/// <summary>Returns an observable that: publishes a <see cref="Unit"/> when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<Unit> SignalAsObservable(this Node node, StringName signalName)
{
return node.SignalAsObservable(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes data of type <typeparamref name="T"/> when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<T> SignalAsObservable<[MustBeVariant] T>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3, T4)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3, T4>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3, T4, T5)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6, T7>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>, <typeparamref name="T8"/>) when <paramref name="node"/> emits the specified signal (except for <see cref="Node.SignalName.TreeExited"/>); finishes when <paramref name="node"/> emits <see cref="Node.SignalName.TreeExited"/>.</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7, T8)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(this Node node, StringName signalName)
{
return node.SignalAsObservable<T0, T1, T2, T3, T4, T5, T6, T7, T8>(signalName, node.CancelOnSignal(Node.SignalName.TreeExited));
}
/// <summary>Returns an observable that: publishes a <see cref="Unit"/> when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<Unit> SignalAsObservable(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes data of type <typeparamref name="T"/> when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<T> SignalAsObservable<[MustBeVariant] T>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3, T4)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3, T4>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3, T4, T5)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6, T7>(obj, signalName, cancellationToken).RefCount();
}
/// <summary>Returns an observable that: publishes a tuple (<typeparamref name="T0"/>, <typeparamref name="T1"/>, <typeparamref name="T2"/>, <typeparamref name="T3"/>, <typeparamref name="T4"/>, <typeparamref name="T5"/>, <typeparamref name="T6"/>, <typeparamref name="T7"/>, <typeparamref name="T8"/>) when <paramref name="obj"/> emits the specified signal; finishes when <paramref name="cancellationToken"/> cancels, or when all subscriptions are disposed, if only <paramref name="cancellationToken"/> is default (= <see cref="CancellationToken.None"/>).</summary>
public static Observable<(T0, T1, T2, T3, T4, T5, T6, T7, T8)> SignalAsObservable<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(this GodotObject obj, StringName signalName, CancellationToken cancellationToken = default)
{
return new GodotSignalMapper<T0, T1, T2, T3, T4, T5, T6, T7, T8>(obj, signalName, cancellationToken).RefCount();
}
}
internal abstract class GodotSignalMapperBase<T> : ConnectableObservable<T>, IDisposable
{
protected readonly Subject<T> subject = new();
private readonly GodotObject godotObject;
private readonly StringName godotSignalName;
private readonly CancellationTokenRegistration? cancellationTokenRegistration;
private Callable? godotCallableOnNext;
private int connected;
private int disposed;
private bool ShouldDisposeOnDisconnect => !cancellationTokenRegistration.HasValue;
protected GodotSignalMapperBase(GodotObject obj, StringName signalName, CancellationToken cancellationToken)
{
godotObject = obj;
godotSignalName = signalName;
if (cancellationToken.CanBeCanceled)
{
cancellationTokenRegistration = cancellationToken.UnsafeRegister((state) => ((GodotSignalMapperBase<T>) state!).Dispose(), this);
}
}
protected override IDisposable SubscribeCore(Observer<T> observer)
{
return subject.Subscribe(observer.Wrap());
}
public override IDisposable Connect()
{
if (Interlocked.CompareExchange(ref connected, 1, 0) == 0)
{
godotCallableOnNext ??= OnNextAsGodotCallable();
godotObject.Connect(godotSignalName, godotCallableOnNext.Value);
}
return new Connection(this);
}
private void Disconnect()
{
if (Interlocked.CompareExchange(ref connected, 0, 1) == 1)
{
godotObject.Disconnect(godotSignalName, godotCallableOnNext!.Value);
}
}
private class Connection(GodotSignalMapperBase<T> parent) : IDisposable
{
public void Dispose()
{
parent.Disconnect();
if (parent.ShouldDisposeOnDisconnect) { parent.Dispose(); }
}
}
public void Dispose()
{
if (Interlocked.CompareExchange(ref disposed, 1, 0) == 1) { return; }
Disconnect();
subject.Dispose(true);
cancellationTokenRegistration?.Dispose();
}
protected abstract Callable OnNextAsGodotCallable();
}
internal class GodotSignalMapper(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<Unit>(obj, signalName, cancellationToken) {
protected override Callable OnNextAsGodotCallable() => Callable.From((Action) OnNextWithUnit);
private void OnNextWithUnit() => subject.OnNext(Unit.Default);
}
internal class GodotSignalMapper<[MustBeVariant] T>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<T>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T>) OnNextWithArgs);
private void OnNextWithArgs(T _a) => subject.OnNext(_a);
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1) => subject.OnNext((_0, _1));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2) => subject.OnNext((_0, _1, _2));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3) => subject.OnNext((_0, _1, _2, _3));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4) => subject.OnNext((_0, _1, _2, _3, _4));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5) => subject.OnNext((_0, _1, _2, _3, _4, _5));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6, T7)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6, T7>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6, T7 _7) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6, _7));
}
internal class GodotSignalMapper<[MustBeVariant] T0, [MustBeVariant] T1, [MustBeVariant] T2, [MustBeVariant] T3, [MustBeVariant] T4, [MustBeVariant] T5, [MustBeVariant] T6, [MustBeVariant] T7, [MustBeVariant] T8>(GodotObject obj, StringName signalName, CancellationToken cancellationToken) : GodotSignalMapperBase<(T0, T1, T2, T3, T4, T5, T6, T7, T8)>(obj, signalName, cancellationToken)
{
protected override Callable OnNextAsGodotCallable() => Callable.From((Action<T0, T1, T2, T3, T4, T5, T6, T7, T8>) OnNextWithArgs);
private void OnNextWithArgs(T0 _0, T1 _1, T2 _2, T3 _3, T4 _4, T5 _5, T6 _6, T7 _7, T8 _8) => subject.OnNext((_0, _1, _2, _3, _4, _5, _6, _7, _8));
}

View File

@@ -0,0 +1 @@
uid://cwsypls11xo1t

View File

@@ -0,0 +1,196 @@
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
namespace R3;
public class GodotTimeProvider : TimeProvider
{
public static readonly GodotTimeProvider Process = new GodotTimeProvider(GodotFrameProvider.Process);
public static readonly GodotTimeProvider PhysicsProcess = new GodotTimeProvider(GodotFrameProvider.PhysicsProcess);
readonly GodotFrameProvider frameProvider;
internal double time;
GodotTimeProvider(FrameProvider frameProvider)
{
this.frameProvider = (GodotFrameProvider)frameProvider;
}
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
return new FrameTimer(callback, state, dueTime, period, frameProvider);
}
public override long GetTimestamp()
{
return TimeSpan.FromSeconds(time).Ticks;
}
}
internal sealed class FrameTimer : ITimer, IFrameRunnerWorkItem
{
enum RunningState
{
Stop,
RunningDueTime,
RunningPeriod,
ChangeRequested
}
readonly TimerCallback callback;
readonly object? state;
readonly GodotFrameProvider frameProvider;
readonly object gate = new object();
TimeSpan dueTime;
TimeSpan period;
RunningState runningState;
double elapsed;
bool isDisposed;
public FrameTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period, GodotFrameProvider frameProvider)
{
this.callback = callback;
this.state = state;
this.dueTime = dueTime;
this.period = period;
this.frameProvider = frameProvider;
Change(dueTime, period);
}
public bool Change(TimeSpan dueTime, TimeSpan period)
{
if (isDisposed) return false;
lock (gate)
{
this.dueTime = dueTime;
this.period = period;
if (dueTime == Timeout.InfiniteTimeSpan)
{
if (runningState == RunningState.Stop)
{
return true;
}
}
if (runningState == RunningState.Stop)
{
frameProvider.Register(this);
}
runningState = RunningState.ChangeRequested;
}
return true;
}
bool IFrameRunnerWorkItem.MoveNext(long frameCount)
{
if (isDisposed) return false;
RunningState runState;
TimeSpan p; // period
TimeSpan d; // dueTime
lock (gate)
{
runState = runningState;
if (runState == RunningState.ChangeRequested)
{
elapsed = 0;
if (dueTime == Timeout.InfiniteTimeSpan)
{
runningState = RunningState.Stop;
return false;
}
runState = runningState = RunningState.RunningDueTime;
}
p = period;
d = dueTime;
}
elapsed += frameProvider.Delta.Value;
try
{
if (runState == RunningState.RunningDueTime)
{
var dt = (double)d.TotalSeconds;
if (elapsed >= dt)
{
callback(state);
elapsed = 0;
if (period == Timeout.InfiniteTimeSpan)
{
return ChangeState(RunningState.Stop);
}
else
{
return ChangeState(RunningState.RunningPeriod);
}
}
else
{
return true;
}
}
else
{
var dt = (double)p.TotalSeconds;
if (elapsed >= dt)
{
callback(state);
elapsed = 0;
}
return ChangeState(RunningState.RunningPeriod);
}
}
catch (Exception ex)
{
ObservableSystem.GetUnhandledExceptionHandler().Invoke(ex);
return ChangeState(RunningState.Stop);
}
}
bool ChangeState(RunningState state)
{
lock (gate)
{
// change requested is high priority
if (runningState == RunningState.ChangeRequested)
{
return true;
}
switch (state)
{
case RunningState.RunningPeriod:
runningState = state;
return true;
default: // otherwise(Stop)
runningState = state;
return false;
}
}
}
public void Dispose()
{
Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
isDisposed = true;
}
public ValueTask DisposeAsync()
{
Dispose();
return default;
}
}

View File

@@ -0,0 +1 @@
uid://dvccr1jhyokui

View File

@@ -0,0 +1,82 @@
using System;
using System.Threading;
using Godot;
namespace R3;
public static class GodotUINodeExtensions
{
public static IDisposable SubscribeToLabel(this Observable<string> source, Label label)
{
return source.Subscribe(label, static (x, l) => l.Text = x);
}
public static IDisposable SubscribeToLabel<T>(this Observable<T> source, Label label)
{
return source.Subscribe(label, static (x, l) => l.Text = x?.ToString());
}
public static IDisposable SubscribeToLabel<T>(this Observable<T> source, Label label, Func<T, string> selector)
{
return source.Subscribe((label, selector), static (x, state) => state.label.Text = state.selector(x));
}
/// <summary>Observe Pressed event.</summary>
public static Observable<Unit> OnPressedAsObservable(this BaseButton button, CancellationToken cancellationToken = default)
{
return Observable.FromEvent(h => button.Pressed += h, h => button.Pressed -= h, cancellationToken);
}
/// <summary>Observe Toggled with current `ButtonPressed` value on subscribe.</summary>
public static Observable<bool> OnToggledAsObservable(this BaseButton button, CancellationToken cancellationToken = default)
{
if (!button.ToggleMode) return Observable.Empty<bool>();
return Observable.Create<bool, (BaseButton, CancellationToken)>((button, cancellationToken), static (observer, state) =>
{
var (b, cancellationToken) = state;
observer.OnNext(b.ButtonPressed);
return Observable.FromEvent<BaseButton.ToggledEventHandler, bool>(h => new BaseButton.ToggledEventHandler(h), h => b.Toggled += h, h => b.Toggled -= h, cancellationToken).Subscribe(observer);
});
}
/// <summary>Observe ValueChanged with current `Value` on subscribe.</summary>
public static Observable<double> OnValueChangedAsObservable(this Godot.Range range, CancellationToken cancellationToken = default)
{
return Observable.Create<double, (Godot.Range, CancellationToken)>((range, cancellationToken), static (observer, state) =>
{
var (s, cancellationToken) = state;
observer.OnNext(s.Value);
return Observable.FromEvent<Godot.Range.ValueChangedEventHandler, double>(h => new Godot.Range.ValueChangedEventHandler(h), h => s.ValueChanged += h, h => s.ValueChanged -= h, cancellationToken).Subscribe(observer);
});
}
/// <summary>Observe TextSubmitted event.</summary>
public static Observable<string> OnTextSubmittedAsObservable(this LineEdit lineEdit, CancellationToken cancellationToken = default)
{
return Observable.FromEvent<LineEdit.TextSubmittedEventHandler, string>(h => new LineEdit.TextSubmittedEventHandler(h), h => lineEdit.TextSubmitted += h, h => lineEdit.TextSubmitted -= h, cancellationToken);
}
/// <summary>Observe TextChanged event.</summary>
public static Observable<string> OnTextChangedAsObservable(this LineEdit lineEdit, CancellationToken cancellationToken = default)
{
return Observable.FromEvent<LineEdit.TextChangedEventHandler, string>(h => new LineEdit.TextChangedEventHandler(h), h => lineEdit.TextChanged += h, h => lineEdit.TextChanged -= h, cancellationToken);
}
/// <summary>Observe TextChanged event.</summary>
public static Observable<Unit> OnTextChangedAsObservable(this TextEdit textEdit, CancellationToken cancellationToken = default)
{
return Observable.FromEvent(h => textEdit.TextChanged += h, h => textEdit.TextChanged -= h, cancellationToken);
}
/// <summary>Observe ItemSelected with current `Selected` on subscribe.</summary>
public static Observable<long> OnItemSelectedAsObservable(this OptionButton optionButton, CancellationToken cancellationToken = default)
{
return Observable.Create<long, (OptionButton, CancellationToken)>((optionButton, cancellationToken), static (observer, state) =>
{
var (b, cancellationToken) = state;
observer.OnNext(b.Selected);
return Observable.FromEvent<OptionButton.ItemSelectedEventHandler, long>(h => new OptionButton.ItemSelectedEventHandler(h), h => b.ItemSelected += h, h => b.ItemSelected -= h, cancellationToken).Subscribe(observer);
});
}
}

View File

@@ -0,0 +1 @@
uid://syarh3o2acgh

View File

@@ -0,0 +1,150 @@
#if TOOLS
#nullable enable
using Godot;
using System;
using System.Collections.Generic;
using GDArray = Godot.Collections.Array;
namespace R3;
// ObservableTrackerDebuggerPlugin creates the Observable Tracker tab in the debugger, and communicates with ObservableTrackerRuntimeHook via EditorDebuggerSessions.
[Tool]
public partial class ObservableTrackerDebuggerPlugin : EditorDebuggerPlugin
{
// Shared header used in IPC by ObservableTracker classes.
public const string MessageHeader = "ObservableTracker";
// Implemented by ObservableTrackerRuntimeHook.
public const string Message_RequestActiveTasks = "RequestActiveTasks";
public const string Message_SetEnableStates = "SetEnableStates";
public const string Message_InvokeGCCollect = "InvokeGCCollect";
// Implemented by ObservableTrackerDebuggerPlugin.
public const string Message_ReceiveActiveTasks = "ReceiveActiveTasks";
// A TrackerSession isolates each debugger session's states.
// There's no way to know if a session has been disposed for good, so we will never remove anything from this dictionary.
// This is similar to how it is handled in the Godot core (see: https://github.com/godotengine/godot/blob/master/modules/multiplayer/editor/multiplayer_editor_plugin.cpp)
readonly Dictionary<int, TrackerSession> sessions = new();
private class TrackerSession
{
public readonly EditorDebuggerSession debuggerSession;
public readonly List<TrackingState> states = new();
public event Action<IEnumerable<TrackingState>>? ReceivedActiveTasks;
public TrackerSession(EditorDebuggerSession debuggerSession)
{
this.debuggerSession = debuggerSession;
}
public void InvokeReceivedActiveTasks()
{
ReceivedActiveTasks?.Invoke(states);
}
}
public override void _SetupSession(int sessionId)
{
var currentSession = GetSession(sessionId);
sessions[sessionId] = new TrackerSession(currentSession);
// NotifyOnSessionSetup gives the tab a reference to the debugger plugin, as well as the sessionId which is needed for messages.
var tab = new ObservableTrackerTab();
tab.NotifyOnSessionSetup(this, sessionId);
currentSession.AddSessionTab(tab);
// As sessions don't seem to be ever disposed, we don't need to unregister these callbacks either.
currentSession.Started += () =>
{
if (IsInstanceValid(tab))
{
tab.SetProcess(true);
// Important! We need to tell the tab the session has started, so it can initialize the enabled states of the runtime ObservableTracker.
tab.NotifyOnSessionStart();
}
};
currentSession.Stopped += () =>
{
if (IsInstanceValid(tab))
{
tab.SetProcess(false);
}
};
}
public override bool _HasCapture(string capture)
{
return capture == MessageHeader;
}
public override bool _Capture(string message, GDArray data, int sessionId)
{
// When EditorDebuggerPlugin._Capture receives messages, the header isn't trimmed (unlike how it is in EngineDebugger),
// so we need to trim it here.
string messageWithoutHeader = message.Substring(message.IndexOf(':') + 1);
//GD.Print(nameof(ObservableTrackerDebuggerPlugin) + " received " + messageWithoutHeader);
switch(messageWithoutHeader)
{
case Message_ReceiveActiveTasks:
// Only invoke event if updated.
if (data[0].AsBool())
{
var session = sessions[sessionId];
session.states.Clear();
foreach (GDArray item in data[1].AsGodotArray())
{
var state = new TrackingState()
{
TrackingId = item[0].AsInt32(),
FormattedType = item[1].AsString(),
AddTime = new DateTime(item[2].AsInt64()),
StackTrace = item[3].AsString(),
};;
session.states.Add(state);
}
session.InvokeReceivedActiveTasks();
}
return true;
}
return base._Capture(message, data, sessionId);
}
public void RegisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action)
{
if (sessions.Count > 0)
sessions[sessionId].ReceivedActiveTasks += action;
}
public void UnregisterReceivedActiveTasks(int sessionId, Action<IEnumerable<TrackingState>> action)
{
if (sessions.Count > 0)
sessions[sessionId].ReceivedActiveTasks -= action;
}
public void UpdateTrackingStates(int sessionId, bool forceUpdate = false)
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_RequestActiveTasks, new () { forceUpdate });
}
}
public void SetEnableStates(int sessionId, bool enableTracking, bool enableStackTrace)
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_SetEnableStates, new () { enableTracking, enableStackTrace});
}
}
public void InvokeGCCollect(int sessionId)
{
if (sessions.Count > 0 && sessions[sessionId].debuggerSession.IsActive())
{
sessions[sessionId].debuggerSession.SendMessage(MessageHeader + ":" + Message_InvokeGCCollect);
}
}
}
#endif

View File

@@ -0,0 +1 @@
uid://bhe85bu3rgxja

View File

@@ -0,0 +1,58 @@
#nullable enable
using Godot;
using System;
using GDArray = Godot.Collections.Array;
namespace R3;
// Sends runtime ObservableTracker information to ObservableTrackerDebuggerPlugin.
// Needs to be an Autoload. Should not be instantiated manually.
public partial class ObservableTrackerRuntimeHook : Node
{
public override void _Ready()
{
#if TOOLS
EngineDebugger.RegisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader, Callable.From((string message, GDArray data) =>
{
//GD.Print(nameof(ObservableTrackerRuntimeHook) + " received " + message);
switch (message)
{
case ObservableTrackerDebuggerPlugin.Message_RequestActiveTasks:
// data[0]: If true, force an update anyway.
if (ObservableTracker.CheckAndResetDirty() || data[0].AsBool())
{
GDArray states = new();
ObservableTracker.ForEachActiveTask(state =>
{
// DateTime is not a Variant type, so we serialize it using Ticks instead.
states.Add(new GDArray { state.TrackingId, state.FormattedType, state.AddTime.Ticks, state.StackTrace });
});
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { true, states });
}
else
{
EngineDebugger.SendMessage(ObservableTrackerDebuggerPlugin.MessageHeader + ":" + ObservableTrackerDebuggerPlugin.Message_ReceiveActiveTasks, new () { false, });
}
break;
case ObservableTrackerDebuggerPlugin.Message_SetEnableStates:
ObservableTracker.EnableTracking = data[0].AsBool();
ObservableTracker.EnableStackTrace = data[1].AsBool();
break;
case ObservableTrackerDebuggerPlugin.Message_InvokeGCCollect:
GC.Collect(0);
break;
}
return true;
}));
#endif
}
public override void _ExitTree()
{
#if TOOLS
EngineDebugger.UnregisterMessageCapture(ObservableTrackerDebuggerPlugin.MessageHeader);
#endif
}
}

View File

@@ -0,0 +1 @@
uid://dwd2efc46qc0v

View File

@@ -0,0 +1,146 @@
#if TOOLS
#nullable enable
using Godot;
using System;
namespace R3;
[Tool]
public partial class ObservableTrackerTab : VBoxContainer
{
public const string EnableAutoReloadKey = "ObservableTracker_EnableAutoReloadKey";
public const string EnableTrackingKey = "ObservableTracker_EnableTrackingKey";
public const string EnableStackTraceKey = "ObservableTracker_EnableStackTraceKey";
bool enableAutoReload, enableTracking, enableStackTrace;
ObservableTrackerTree? tree;
ObservableTrackerDebuggerPlugin? debuggerPlugin;
int interval = 0;
int sessionId = 0;
public void NotifyOnSessionSetup(ObservableTrackerDebuggerPlugin debuggerPlugin, int sessionId)
{
this.debuggerPlugin = debuggerPlugin;
this.sessionId = sessionId;
tree ??= new ObservableTrackerTree();
tree.NotifyOnSessionSetup(debuggerPlugin!, sessionId);
}
public void NotifyOnSessionStart()
{
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
}
public override void _Ready()
{
Name = "Observable Tracker";
tree ??= new ObservableTrackerTree();
// Head panel
var headPanelLayout = new HBoxContainer();
headPanelLayout.SetAnchor(Side.Left, 0);
headPanelLayout.SetAnchor(Side.Right, 0);
AddChild(headPanelLayout);
// Toggle buttons (top left)
var enableAutoReloadToggle = new CheckButton
{
Text = "Enable AutoReload",
TooltipText = "Reload automatically."
};
var enableTrackingToggle = new CheckButton
{
Text = "Enable Tracking",
TooltipText = "Start to track Observable subscription. Performance impact: low"
};
var enableStackTraceToggle = new CheckButton
{
Text = "Enable StackTrace",
TooltipText = "Capture StackTrace when subscribed. Performance impact: high"
};
// For every button: Initialize pressed state and subscribe to Toggled event.
EditorSettings settings = EditorInterface.Singleton.GetEditorSettings();
enableAutoReloadToggle.ButtonPressed = enableAutoReload = GetSettingOrDefault(settings, EnableAutoReloadKey, false).AsBool();
enableAutoReloadToggle.Toggled += toggledOn =>
{
settings.SetSetting(EnableAutoReloadKey, toggledOn);
enableAutoReload = toggledOn;
};
enableTrackingToggle.ButtonPressed = enableTracking = GetSettingOrDefault(settings, EnableTrackingKey, false).AsBool();
enableTrackingToggle.Toggled += toggledOn =>
{
settings.SetSetting(EnableTrackingKey, toggledOn);
enableTracking = toggledOn;
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
};
enableStackTraceToggle.ButtonPressed = enableStackTrace = GetSettingOrDefault(settings, EnableStackTraceKey, false).AsBool();
enableStackTraceToggle.Toggled += toggledOn =>
{
settings.SetSetting(EnableStackTraceKey, toggledOn);
enableStackTrace = toggledOn;
debuggerPlugin!.SetEnableStates(sessionId, enableTracking, enableStackTrace);
};
// Regular buttons (top right)
var reloadButton = new Button
{
Text = "Reload",
TooltipText = "Reload View."
};
var GCButton = new Button
{
Text = "GC.Collect",
TooltipText = "Invoke GC.Collect."
};
reloadButton.Pressed += () =>
{
debuggerPlugin!.UpdateTrackingStates(sessionId, true);
};
GCButton.Pressed += () =>
{
debuggerPlugin!.InvokeGCCollect(sessionId);
};
// Button layout.
headPanelLayout.AddChild(enableAutoReloadToggle);
headPanelLayout.AddChild(enableTrackingToggle);
headPanelLayout.AddChild(enableStackTraceToggle);
// Kind of like Unity's FlexibleSpace. Pushes the first three buttons to the left, and the remaining buttons to the right.
headPanelLayout.AddChild(new Control()
{
SizeFlagsHorizontal = SizeFlags.Expand,
});
headPanelLayout.AddChild(reloadButton);
headPanelLayout.AddChild(GCButton);
// Tree goes last.
AddChild(tree);
}
public override void _Process(double delta)
{
if (enableAutoReload)
{
if (interval++ % 120 == 0)
{
debuggerPlugin!.UpdateTrackingStates(sessionId);
}
}
}
static Variant GetSettingOrDefault(EditorSettings settings, string key, Variant @default)
{
if (settings.HasSetting(key))
{
return settings.GetSetting(key);
}
else
{
return @default;
}
}
}
#endif

View File

@@ -0,0 +1 @@
uid://c2u5u8xkqvh1c

View File

@@ -0,0 +1,67 @@
#if TOOLS
#nullable enable
using Godot;
using System;
using System.Collections;
using System.Collections.Generic;
namespace R3;
[Tool]
public partial class ObservableTrackerTree : Tree
{
ObservableTrackerDebuggerPlugin? debuggerPlugin;
int sessionId;
public void NotifyOnSessionSetup(ObservableTrackerDebuggerPlugin debuggerPlugin, int sessionId)
{
this.debuggerPlugin = debuggerPlugin;
this.sessionId = sessionId;
debuggerPlugin!.RegisterReceivedActiveTasks(sessionId, Reload);
Clear();
}
public override void _Ready()
{
AllowReselect = false;
Columns = 3;
ColumnTitlesVisible = true;
SetColumnTitle(0, "Type");
SetColumnTitle(1, "Elapsed");
SetColumnTitle(2, "StackTrace");
SetColumnExpand(0, true);
SetColumnExpand(1, true);
SetColumnExpand(2, true);
SetColumnExpandRatio(0, 3);
SetColumnExpandRatio(1, 1);
SetColumnExpandRatio(2, 6);
SetColumnClipContent(0, true);
SetColumnClipContent(1, true);
SetColumnClipContent(2, true);
HideRoot = true;
SizeFlagsVertical = SizeFlags.ExpandFill;
}
public override void _ExitTree()
{
debuggerPlugin!.UnregisterReceivedActiveTasks(sessionId, Reload);
}
public void Reload(IEnumerable<TrackingState> states)
{
Clear();
TreeItem root = CreateItem();
foreach(TrackingState state in states)
{
TreeItem row = CreateItem(root);
var now = DateTime.Now;
// Type
row.SetText(0, state.FormattedType);
// Elapsed
row.SetText(1, (now - state.AddTime).TotalSeconds.ToString("00.00"));
// StackTrace
row.SetText(2, state.StackTrace);
};
}
}
#endif

View File

@@ -0,0 +1 @@
uid://crct8t2w3hcce

View File

@@ -0,0 +1,8 @@
[plugin]
name="R3.Godot"
description="The new future of dotnet/reactive and UniRx."
author="Cysharp"
version="1.3.0"
language="C-sharp"
script="GodotR3Plugin.cs"

View File

@@ -17,6 +17,11 @@ run/max_fps=157
run/low_processor_mode=true
config/icon="res://icon.svg"
[autoload]
FrameProviderDispatcher="*res://addons/R3.Godot/FrameProviderDispatcher.cs"
ObservableTrackerRuntimeHook="*res://addons/R3.Godot/ObservableTrackerRuntimeHook.cs"
[display]
window/energy_saving/keep_screen_on=false
@@ -25,6 +30,10 @@ window/energy_saving/keep_screen_on=false
project/assembly_name="SharpIDE.Godot"
[editor_plugins]
enabled=PackedStringArray("res://addons/R3.Godot/plugin.cfg", "res://addons/csharp_gdextension_bindgen/plugin.cfg")
[input]
CodeFixes={