项目文件夹

文件
2026-07-13 13:13:17 +08:00

714 行
29 KiB
C#

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
using ImGuiNET;
using T3.Core.Animation;
using T3.Core.Audio;
using T3.Core.DataTypes.DataSet;
using T3.Core.DataTypes.Vector;
using T3.Core.IO;
using T3.Core.Operator;
using T3.Core.Settings;
using T3.Core.Utils;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Interaction.Keyboard;
using T3.Editor.Gui.Interaction.Timing;
using T3.Editor.Gui.OutputUi;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.Gui.Windows.Layouts;
using T3.Editor.UiModel;
using T3.Editor.UiModel.InputsAndTypes;
using Icon = T3.Editor.Gui.Styling.Icon;
using Vector2 = System.Numerics.Vector2;
using Vector4 = System.Numerics.Vector4;
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace T3.Editor.Gui.Windows.TimeLine;
[HelpUiID("ControlBar")]
internal static class TimeControls
{
internal static void HandleTimeControlActions()
{
var playback = Playback.Current; // TODO, this should be non-static eventually
if (UserActions.TapBeatSync.Triggered())
BeatTiming.TriggerSyncTap();
if (UserActions.TapBeatSyncMeasure.Triggered())
BeatTiming.TriggerResyncMeasure();
if (UserActions.PlaybackJumpToPreviousKeyframe.Triggered())
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToPreviousKeyframe);
if (UserActions.PlaybackJumpToStartTime.Triggered())
playback.TimeInBars = playback.IsLooping ? playback.LoopRange.Start : 0;
if (UserActions.PlaybackJumpToPreviousKeyframe.Triggered())
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToPreviousKeyframe);
{
//const float editFrameRate = 30;
var frameDuration = UserSettings.Config.FrameStepAmount switch
{
FrameStepAmount.FrameAt60Fps => 1 / 60f,
FrameStepAmount.FrameAt30Fps => 1 / 30f,
FrameStepAmount.FrameAt15Fps => 1 / 15f,
FrameStepAmount.Bar => (float)playback.SecondsFromBars(1),
FrameStepAmount.Beat => (float)playback.SecondsFromBars(1 / 4f),
FrameStepAmount.Tick => (float)playback.SecondsFromBars(1 / 16f),
_ => 1
};
var editFrameRate = 1 / frameDuration;
// Step to previous frame
if (UserActions.PlaybackPreviousFrame.Triggered())
{
var rounded = Math.Round(playback.TimeInSecs * editFrameRate) / editFrameRate;
playback.TimeInSecs = rounded - frameDuration;
}
if (UserActions.PlaybackJumpBack.Triggered())
{
playback.TimeInBars -= 1;
}
// Step to next frame
if (UserActions.PlaybackNextFrame.Triggered())
{
var rounded = Math.Round(playback.TimeInSecs * editFrameRate) / editFrameRate;
playback.TimeInSecs = rounded + frameDuration;
}
}
// Play backwards with increasing speed
if (UserActions.PlaybackBackwards.Triggered())
{
Log.Debug("Backwards triggered with speed " + playback.PlaybackSpeed);
if (playback.PlaybackSpeed >= 0)
{
playback.PlaybackSpeed = -1;
}
else if (playback.PlaybackSpeed > -16)
{
playback.PlaybackSpeed *= 2;
}
}
// Play forward with increasing speed
if (UserActions.PlaybackForward.Triggered())
{
if (playback.PlaybackSpeed <= 0)
{
_lastPlaybackStartTime = playback.TimeInBars;
playback.PlaybackSpeed = 1;
}
else if (playback.PlaybackSpeed < 16) // Bass can't play much faster anyways
{
playback.PlaybackSpeed *= 2;
}
}
if (UserActions.PlaybackForwardHalfSpeed.Triggered())
{
if (playback.PlaybackSpeed is > 0 and < 1f)
playback.PlaybackSpeed *= 0.5f;
else
playback.PlaybackSpeed = 0.5f;
}
// Stop as separate keyboard action
if (UserActions.PlaybackStop.Triggered())
{
playback.PlaybackSpeed = 0;
if (UserSettings.Config.ResetTimeAfterPlayback)
playback.TimeInBars = _lastPlaybackStartTime;
}
if (UserActions.PlaybackToggle.Triggered())
{
if (playback.PlaybackSpeed == 0)
{
playback.PlaybackSpeed = 1;
_lastPlaybackStartTime = playback.TimeInBars;
}
else
{
playback.PlaybackSpeed = 0;
if (UserSettings.Config.ResetTimeAfterPlayback)
playback.TimeInBars = _lastPlaybackStartTime;
}
}
if (UserActions.SetStartTime.Triggered())
{
Playback.Current.IsLooping = true;
Playback.Current.LoopRange.Start = (float)Playback.Current.TimeInBars;
if(Playback.Current.LoopRange.End < Playback.Current.LoopRange.Start)
Playback.Current.LoopRange.End = Playback.Current.LoopRange.Start + 4;
}
if (UserActions.SetEndTime.Triggered())
{
Playback.Current.IsLooping = true;
Playback.Current.LoopRange.End = (float)Playback.Current.TimeInBars;
if(Playback.Current.LoopRange.Start > Playback.Current.LoopRange.End)
Playback.Current.LoopRange.Start = Playback.Current.LoopRange.End - 4;
}
if (UserActions.PlaybackJumpToNextKeyframe.Triggered())
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToNextKeyframe);
if (UserActions.PlaybackJumpToPreviousKeyframe.Triggered())
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToPreviousKeyframe);
}
internal static void DrawTimeControls(TimeLineCanvas timeLineCanvas, Instance composition)
{
var playback = Playback.Current;
// Settings
PlaybackUtils.FindCompositionSettingsForInstance(composition, out var compositionWithSettings, out var settings);
var opHasSettings = compositionWithSettings == composition;
// if (CustomComponents.IconButton(CoreSettings.Config.AudioMuted ? Icon.ToggleAudioOff : Icon.ToggleAudioOn,
// ControlSize,
// CoreSettings.Config.AudioMuted
// ? CustomComponents.ButtonStates.NeedsAttention
// : CustomComponents.ButtonStates.Default
// ))
// {
// CoreSettings.Config.AudioMuted = !CoreSettings.Config.AudioMuted;
// AudioEngine.SetMute(CoreSettings.Config.AudioMuted);
// }
if (CustomComponents.IconButton(Icon.Settings, ControlSize, opHasSettings
? CustomComponents.ButtonStates.Emphasized
: CustomComponents.ButtonStates.Default))
{
WindowManager.ToggleInstanceVisibility<ProjectSettingsWindow>();
}
CustomComponents.TooltipForLastItem("Project Settings",
"Switch between soundtrack and VJ modes. Control BPM and other inputs.");
ImGui.SameLine();
// Current Time
var delta = 0.0;
string formattedTime = "";
switch (UserSettings.Config.TimeDisplayMode)
{
case TimeFormat.TimeDisplayModes.Bars:
formattedTime = TimeFormat.FormatTimeInBars(playback.TimeInBars, 0);
break;
case TimeFormat.TimeDisplayModes.Secs:
var ts = TimeSpan.FromSeconds(playback.TimeInSecs);
formattedTime = ts.Hours > 0
? ts.ToString(@"hh\:mm\:ss\:ff")
: ts.ToString(@"mm\:ss\:ff");
break;
case TimeFormat.TimeDisplayModes.F30:
var frames = playback.TimeInSecs * 30;
formattedTime = $"{frames:0}f ";
break;
case TimeFormat.TimeDisplayModes.F60:
var frames60 = playback.TimeInSecs * 60;
formattedTime = $"{frames60:0}f ";
break;
}
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
if (JogDial(formattedTime, ref delta, new Vector2(StandardWidth, ControlSize.Y)))
{
playback.PlaybackSpeed = 0;
playback.TimeInBars += delta;
if (UserSettings.Config.TimeDisplayMode == TimeFormat.TimeDisplayModes.F30)
{
playback.TimeInSecs = Math.Floor(playback.TimeInSecs * 30) / 30;
}
}
ImGui.PopStyleColor();
if(ImGui.IsItemHovered())
CustomComponents.TooltipForLastItem($"Current playtime at {settings.Playback.Bpm:0.0} BPM.", "Click mode button to toggle between timeline formats.");
ImGui.SameLine();
// Time Mode with context menu
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
if (ImGui.Button(UserSettings.Config.TimeDisplayMode.ToString(), ControlSize * new Vector2(1.5f,1)))
{
UserSettings.Config.TimeDisplayMode =
(TimeFormat.TimeDisplayModes)(((int)UserSettings.Config.TimeDisplayMode + 1) % Enum.GetNames(typeof(TimeFormat.TimeDisplayModes)).Length);
}
ImGui.PopStyleColor();
CustomComponents.TooltipForLastItem("Timeline format",
"Click to toggle through BPM, Frames and Normal time modes");
ImGui.SameLine();
// Idle motion
{
ImGui.PushStyleColor(ImGuiCol.Text, UserSettings.Config.EnableIdleMotion
? UiColors.TextDisabled
: new Vector4(0, 0, 0, 0.5f));
// Create invisible button with same size as icon buttons would have
if (ImGui.Button("##idleMotionToggle", ControlSize))
{
UserSettings.Config.EnableIdleMotion = !UserSettings.Config.EnableIdleMotion;
}
// Tooltip (same as before)
CustomComponents.TooltipForLastItem("Idle Motion - Keeps beat time running",
"This will keep updating the output [Time]\nwhich is useful for procedural animation and syncing.");
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
var center = (min + max) * 0.5f;
// Draw beat grid background
const int cellCount = 4;
var cellSize = CellSize;
var gridOffset = center - GridSize;
// Pre-calculate the cell size minus 1 for the rect size
var rectSize = new Vector2(cellSize - 1, cellSize - 1);
var gridColor = UserSettings.Config.EnableIdleMotion ? UiColors.BackgroundFull.Fade(0.2f) : UiColors.ForegroundFull.Fade(0.1f);
for (int x = 0; x < cellCount; x++)
{
for (int y = 0; y < cellCount; y++)
{
var cellMin = gridOffset + new Vector2(x * cellSize, y * cellSize);
var cellMax = cellMin + rectSize;
drawList.AddRectFilled(cellMin, cellMax, gridColor);
}
}
if (!UserSettings.Config.EnableIdleMotion)
{
var diagonal = new Vector2(CellSize, -CellSize) * 2.3f;
var lineStart = center + diagonal;
var lineEnd = center - diagonal;
drawList.AddLine(lineStart,
lineEnd,
UiColors.BackgroundPopup.Fade(0.4f),5);
drawList.AddLine(lineStart,
lineEnd,
UiColors.ForegroundFull.Fade(0.1f));
}
// If idle motion is enabled, draw the animated indicator
if (UserSettings.Config.EnableIdleMotion)
{
var beat = (int)(playback.FxTimeInBars * 4) % 4;
var beatPulse = (playback.FxTimeInBars * 4) % 4 - beat;
var bar = (int)(playback.FxTimeInBars) % 4;
if (beat < 0)
beat = 4 + beat;
if (bar < 0)
bar = 4 + bar;
var indicatorMin = gridOffset + new Vector2(beat * cellSize, bar * cellSize);
var indicatorMax = indicatorMin + new Vector2(cellSize - 1, cellSize - 1);
drawList.AddRectFilled(indicatorMin, indicatorMax,
Color.Mix(UiColors.StatusAnimated,
UiColors.BackgroundFull.Fade(0.3f),
(float)beatPulse));
}
ImGui.PopStyleColor();
ImGui.SameLine();
}
// MidiIndicator
{
var timeSinceLastEvent = Playback.RunTimeInSecs - Math.Max(T3Ui.MidiDataRecording.LastEventTime, T3Ui.OscDataRecording.LastEventTime);
var flashFactor = MathF.Pow((float)timeSinceLastEvent.Clamp(0, 1) / 1, 0.5f);
var color = Color.Mix(UiColors.StatusAnimated, UiColors.BackgroundFull.Fade(0.3f), flashFactor);
if (CustomComponents.IconButton(Icon.IO, ControlSize, color))
{
//T3Ui.MidiStreamRecorder.Reset();
//DataRecording.ActiveRecordingSet.WriteToFile();
WindowManager.ToggleInstanceVisibility<IoViewWindow>();
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
if (timeSinceLastEvent < 10)
{
ImGui.BeginChild("canvas", new Vector2(400, 250));
//DataSetOutputUi.DrawDataSet(dataSet);
_dataSetView.Draw(DataRecording.ActiveRecordingSet);
ImGui.EndChild();
}
else
{
ImGui.Text("Midi and OSC input indicator\nClick to open IO window.");
}
ImGui.EndTooltip();
}
ImGui.SameLine();
}
if (settings.Playback.Syncing == CompositionSettings.SyncModes.Tapping)
{
var bpm = BeatTiming.Bpm;
if (SingleValueEdit.Draw(ref bpm, new Vector2(StandardWidth, ControlSize.Y), min: 1, max: 360, clampMin: true, clampMax: true, scale: 0.01f, format: "{0:0.0 BPM}") ==
InputEditStateFlags.Modified)
{
composition.Symbol.GetSymbolUi().FlagAsModified();
BeatTiming.SetBpmRate(bpm);
}
ImGui.SameLine();
ImGui.Button("Sync", SyncButton);
if (ImGui.IsItemHovered())
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
if (ImGui.GetIO().KeyCtrl)
{
var roundedBpm = Math.Round(BeatTiming.Bpm*2)/2;
BeatTiming.SetBpmRate((float)roundedBpm);
}
else
{
BeatTiming.TriggerSyncTap();
}
}
else if (ImGui.IsMouseClicked(ImGuiMouseButton.Right))
{
BeatTiming.TriggerResyncMeasure();
}
}
CustomComponents.TooltipForLastItem("Click on beat to sync. Tap later once to refine. Click right to sync measure.\n"
+ "Ctrl+Click to round BPM",
$"Tap: {UserActions.TapBeatSync.ListShortcuts()}\n"
+ $"Resync: {UserActions.TapBeatSyncMeasure.ListShortcuts()}");
ImGui.SameLine();
}
else
{
// Jump to start
if (CustomComponents.IconButton(Icon.JumpToRangeStart,
ControlSize,
playback.TimeInBars != playback.LoopRange.Start
? CustomComponents.ButtonStates.Default
: CustomComponents.ButtonStates.Disabled
)
|| UserActions.PlaybackJumpToStartTime.Triggered()
)
{
playback.TimeInBars = playback.IsLooping ? playback.LoopRange.Start : 0;
}
CustomComponents.TooltipForLastItem("Jump to beginning",
UserActions.PlaybackJumpToStartTime.ListShortcuts());
ImGui.SameLine();
// Prev Keyframe
if (CustomComponents.IconButton(Icon.JumpToPreviousKeyframe,
ControlSize,
FrameStats.Last.HasKeyframesBeforeCurrentTime
? CustomComponents.ButtonStates.Default
: CustomComponents.ButtonStates.Disabled)
)
{
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToPreviousKeyframe);
}
CustomComponents.TooltipForLastItem("Jump to previous keyframe",
UserActions.PlaybackJumpToPreviousKeyframe.ListShortcuts());
ImGui.SameLine();
// Play backwards
if (CustomComponents.IconButton(Icon.PlayBackwards,
ControlSize,
playback.PlaybackSpeed < 0
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Default
))
{
if (playback.PlaybackSpeed != 0)
{
playback.PlaybackSpeed = 0;
}
else if (playback.PlaybackSpeed == 0)
{
playback.PlaybackSpeed = -1;
}
}
if (playback.PlaybackSpeed < -1)
{
ImGui.GetWindowDrawList().AddText(ImGui.GetItemRectMin() + new Vector2(20, 4), UiColors.ForegroundFull, $"×{-playback.PlaybackSpeed:0}");
}
CustomComponents.TooltipForLastItem("Play backwards",
"Play backwards (and faster): " +
UserActions.PlaybackBackwards.ListShortcuts() +
"\nPrevious frame:" + UserActions.PlaybackPreviousFrame.ListShortcuts());
ImGui.SameLine();
// Play forward
if (CustomComponents.IconButton(Icon.PlayForwards,
ControlSize,
playback.PlaybackSpeed > 0
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Default
))
{
if (Math.Abs(playback.PlaybackSpeed) > 0.001f)
{
playback.PlaybackSpeed = 0;
}
else if (Math.Abs(playback.PlaybackSpeed) < 0.001f)
{
playback.PlaybackSpeed = 1;
}
}
if (playback.PlaybackSpeed > 1)
{
ImGui.GetWindowDrawList().AddText(ImGui.GetItemRectMin() + new Vector2(20, 4), UiColors.ForegroundFull, $"×{playback.PlaybackSpeed:0}");
}
CustomComponents.TooltipForLastItem("Start playback",
"Play forward (and faster): " +
UserActions.PlaybackForward.ListShortcuts() +
"\nPlay half speed (and slower): " +
UserActions.PlaybackForwardHalfSpeed.ListShortcuts() +
"\nNext frame:" + UserActions.PlaybackNextFrame.ListShortcuts());
ImGui.SameLine();
// Next Keyframe
if (CustomComponents.IconButton(Icon.JumpToNextKeyframe,
ControlSize,
FrameStats.Last.HasKeyframesAfterCurrentTime
? CustomComponents.ButtonStates.Default
: CustomComponents.ButtonStates.Disabled)
)
{
UserActionRegistry.QueueAction(UserActions.PlaybackJumpToNextKeyframe);
}
CustomComponents.TooltipForLastItem("Jump to next keyframe",
UserActions.PlaybackJumpToNextKeyframe.ListShortcuts());
ImGui.SameLine();
// // End
// Loop
if (CustomComponents.IconButton(Icon.Loop,
ControlSize,
playback.IsLooping
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Default))
{
playback.IsLooping = !playback.IsLooping;
var loopRangeMatchesTime = playback.LoopRange.IsValid && playback.LoopRange.Contains(playback.TimeInBars);
if (playback.IsLooping && !loopRangeMatchesTime)
{
playback.LoopRange.Start = (float)(playback.TimeInBars - playback.TimeInBars % 4);
playback.LoopRange.Duration = 4;
}
}
CustomComponents.TooltipForLastItem("Loop playback", "This will initialize one bar around current time.");
ImGui.SameLine();
// Curve Mode
var hasKeyframes = FrameStats.Current.HasKeyframesAfterCurrentTime || FrameStats.Current.HasKeyframesAfterCurrentTime;
ImGui.PushStyleColor(ImGuiCol.Text, hasKeyframes ? UiColors.Text.Rgba : UiColors.TextMuted);
if (ImGui.Button(timeLineCanvas.Mode.ToString(), DopeCurve)) //
{
timeLineCanvas.Mode = (TimeLineCanvas.Modes)(((int)timeLineCanvas.Mode + 1) % Enum.GetNames(typeof(TimeLineCanvas.Modes)).Length);
}
ImGui.PopStyleColor();
CustomComponents.TooltipForLastItem("Toggle keyframe view between Dope sheet and Curve mode.");
ImGui.SameLine();
}
// ToggleAudio
if (CustomComponents.IconButton(CoreSettings.Config.AppMute ? Icon.ToggleAudioOff : Icon.ToggleAudioOn,
ControlSize,
CoreSettings.Config.AppMute
? CustomComponents.ButtonStates.NeedsAttention
: CustomComponents.ButtonStates.Default
))
{
AudioEngine.SetGlobalMute(!CoreSettings.Config.AppMute);
}
// ToggleHover
{
ImGui.SameLine();
Icon icon;
string hoverModeTooltip;
string hoverModeAdditionalTooltip = null;
CustomComponents.ButtonStates state = CustomComponents.ButtonStates.Emphasized;
switch (UserSettings.Config.HoverMode)
{
case UserSettings.GraphHoverModes.Disabled:
state = CustomComponents.ButtonStates.Default;
icon = Icon.HoverPreviewDisabled;
hoverModeTooltip = "No preview images on hover";
break;
case UserSettings.GraphHoverModes.Live:
icon = Icon.HoverPreviewPlay;
hoverModeTooltip = "Live Hover Preview - Render explicit thumbnail image.";
hoverModeAdditionalTooltip = "This can interfere with the rendering of the current output.";
break;
default:
icon = Icon.HoverPreviewSmall;
hoverModeTooltip = "Last - Show the current state of the operator.";
hoverModeAdditionalTooltip = "This can be outdated if operator is not require for current output.";
break;
}
if (CustomComponents.IconButton(icon, ControlSize, state))
{
UserSettings.Config.HoverMode =
(UserSettings.GraphHoverModes)(((int)UserSettings.Config.HoverMode + 1) % Enum.GetNames(typeof(UserSettings.GraphHoverModes)).Length);
}
CustomComponents.TooltipForLastItem(hoverModeTooltip, hoverModeAdditionalTooltip);
}
if (FrameStats.Last.HasAnimatedParameters)
{
// Lock all animated parameters
ImGui.SameLine();
var state = UserSettings.Config.AutoPinAllAnimations
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Default;
if (CustomComponents.IconButton(Icon.PinParams, ControlSize, state)
|| UserActions.ToggleAnimationPinning.Triggered())
{
UserSettings.Config.AutoPinAllAnimations = !UserSettings.Config.AutoPinAllAnimations;
if (!UserSettings.Config.AutoPinAllAnimations)
{
timeLineCanvas.DopeSheetArea.PinnedParametersHashes.Clear();
}
}
CustomComponents.TooltipForLastItem("Keep animated parameters visible",
"This can be useful when align animations between multiple operators. Toggle again to clear the visible animations.\n\n"
+ UserActions.ToggleAnimationPinning.ListShortcuts()
);
}
ImGui.SameLine();
}
private static double _lastPlaybackStartTime;
// beat grid optimizations
private static float _lastUiScaleFactor = -1f;
private static float _cachedCellSize;
private static Vector2 _cachedGridSize;
private static float CellSize
{
get
{
if (!(Math.Abs(_lastUiScaleFactor - T3Ui.UiScaleFactor) > 0.001f))
return _cachedCellSize;
_lastUiScaleFactor = T3Ui.UiScaleFactor;
_cachedCellSize = 4f * T3Ui.UiScaleFactor;
_cachedGridSize = new Vector2(_cachedCellSize * 4 * 0.5f);
return _cachedCellSize;
}
}
private static Vector2 GridSize
{
get
{
// Trigger cache update if needed
_ = CellSize;
return _cachedGridSize;
}
}
// end of beat grid optimizations
private static float StandardWidth => 100f * T3Ui.UiScaleFactor;
public static Vector2 ControlSize => new Vector2(35, 28) * T3Ui.UiScaleFactor;
private static Vector2 DopeCurve => new Vector2(95, 28) * T3Ui.UiScaleFactor;
private static Vector2 SyncButton => new Vector2(45, 28) * T3Ui.UiScaleFactor;
private static readonly DataSetViewCanvas _dataSetView = new()
{
ShowInteraction = false,
MaxTreeLevel = 0,
};
public static bool JogDial(string label, ref double delta, Vector2 size)
{
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f));
var isActive = ImGui.Button(label + "###dummy", size);
ImGui.PopStyleVar();
var io = ImGui.GetIO();
if (ImGui.IsItemActive())
{
var center = (ImGui.GetItemRectMin() + ImGui.GetItemRectMax()) * 0.5f;
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
ImGui.GetForegroundDrawList().AddCircle(center, 100, UiColors.Gray, 50);
isActive = true;
var pLast = io.MousePos - io.MouseDelta - center;
var pNow = io.MousePos - center;
var aLast = Math.Atan2(pLast.X, pLast.Y);
var aNow = Math.Atan2(pNow.X, pNow.Y);
delta = aLast - aNow;
if (delta > 1.5)
{
delta -= 2 * Math.PI;
}
else if (delta < -1.5)
{
delta += 2 * Math.PI;
}
}
return isActive;
}
}