Code/Demos/KeystrokeVisualizer/KeystrokeView.cs
using System.Collections.Generic;
using Goo;
using Goo.Animation;
using Goo.Input;
using Sandbox;
using Sandbox.UI;
namespace Sandbox.KeystrokeViz;
// Keystroke chip stack. Polls input each Tick; emits the chip column content (host anchors it).
public sealed class KeystrokeView
{
public int MaxEntries = 4;
public Layout.Anchor Anchor = Layout.Anchor.BottomLeft;
public float AnchorMargin = 32f;
public float HoldTime = 1.5f;
public float SlideInDuration = 0.1f;
public float FadeOutDuration = 0.20f;
public float SlideRiseFraction = 0.1f;
public float IdleGap = 1f;
public float ChipHeight = 64f;
public float ChipGap = 8f;
public float FontSize = 32f;
public float ChipOpacity = 0.8f;
public int MaxTypingRunLength = 16;
public bool ShowHintWhenEmpty = true;
public bool CaptureModifierAlone = false;
public bool ShiftAsChord = true;
readonly List<Entry> _queue = new();
readonly KeyTracker _tracker = new() { ReemitHeldOnModifierRise = true };
float _now;
bool _dirty = true;
void Invalidate() => _dirty = true;
public void Reset()
{
_queue.Clear();
_tracker.Reset();
_now = 0f;
Invalidate();
}
public bool Tick( Scene? scene, float dt )
{
_now += dt;
_tracker.Poll();
int countBefore = _queue.Count;
bool hadInput = _tracker.JustPressed.Count > 0;
if ( hadInput )
{
var cfg = new GroupingConfig( IdleGap, CaptureModifierAlone, ShiftAsChord, MaxTypingRunLength );
KeystrokeGrouper.Apply( _queue, _tracker.JustPressed, _tracker.Modifiers, _now, cfg );
}
KeystrokeGrouper.Sweep( _queue, _now, HoldTime, FadeOutDuration );
KeystrokeGrouper.EnforceCap( _queue, _now, MaxEntries, HoldTime );
if ( hadInput || _queue.Count != countBefore || NeedsRebuild() )
Invalidate();
bool d = _dirty; _dirty = false; return d;
}
bool NeedsRebuild() =>
KeystrokeGrouper.HasAnimatingEntries( _queue, _now, SlideInDuration, HoldTime, FadeOutDuration );
public Container Build()
{
var r = Layout.ResolveAnchor( Anchor );
// HintBanner returns a Text; wrap it so Build() always returns a Container the host can anchor.
if ( _queue.Count == 0 && ShowHintWhenEmpty )
return new Container { Key = "hint", Children = { HintBanner.Build( FontSize ) } };
// keyed so the anchored slot stays all-keyed across the empty/non-empty flip (mixed keyed/unkeyed pair warns).
var column = new Container { Key = "chips", FlexDirection = r.FlexDirection, Gap = ChipGap };
var anim = new ChipAnim( new AgePhase( SlideInDuration, HoldTime, FadeOutDuration ), ChipHeight, SlideRiseFraction, FontSize, ChipOpacity, r.StacksUp );
column.Children.AddRange( _queue, ( i, e ) =>
EntryChip.Build( i, e.Label, _now - e.SpawnTime, _now - e.LastInputTime, anim ) );
return column;
}
}