Editor/MovieMaker/Editor/ScrubberWidget.cs
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
/// <summary>
/// A bar with times and notches on it
/// </summary>
public class ScrubberItem : GraphicsItem
{
public MovieEditor Editor { get; }
public Session Session { get; }
public bool IsTop { get; }
public ScrubberItem( MovieEditor timelineEditor, bool isTop )
{
Session = timelineEditor.Session;
Editor = timelineEditor;
IsTop = isTop;
ZIndex = 5000;
HoverEvents = true;
Selectable = true;
ToolTip =
"""
<h3>Scrubber</h3>
<p><b>Click</b> to set playhead time, <b>Drag</b> to scrub.</p>
<p><b>Alt+Click+Drag</b> to set a loop time range for previewing playback, <b>Alt+Click</b> to clear loop.</p>
""";
}
private MovieTime _dragStartTime;
private float _panSpeed;
public void Frame()
{
Cursor = (Application.KeyboardModifiers & KeyboardModifiers.Alt) != 0
? CursorShape.IBeam
: CursorShape.Finger;
if ( !_panSpeed.AlmostEqual( 0f ) )
{
var delta = -_panSpeed * RealTime.Delta;
Session.ScrollBy( delta, false );
_lastScrub.ScenePos -= delta;
OnScrubUpdate();
}
}
protected override void OnMousePressed( GraphicsMouseEvent e )
{
var time = Session.ScenePositionToTime( ToScene( e.LocalPosition ), new SnapOptions( SnapFlag.Playhead ) );
if ( e.MiddleMouseButton )
{
// Panning handled by timeline
return;
}
if ( e.RightMouseButton )
{
ShowContextMenu( time );
e.Accepted = true;
return;
}
StartScrubbing( time, e.KeyboardModifiers );
e.Accepted = true;
}
public void StartScrubbing( MovieTime time, KeyboardModifiers modifiers )
{
if ( (modifiers & KeyboardModifiers.Alt) != 0 )
{
// Alt+Click: set loop time range
_dragStartTime = time;
Session.LoopTimeRange = null;
}
else
{
// Click: set playhead time
Session.PlayheadTime = time;
}
_panSpeed = 0f;
Update();
}
protected override void OnMouseReleased( GraphicsMouseEvent e )
{
base.OnMouseReleased(e);
StopScrubbing();
}
public void StopScrubbing()
{
_panSpeed = 0f;
}
private void ShowContextMenu( MovieTime time )
{
var menu = new Menu();
menu.AddHeading( "Preview Loop" );
menu.AddOption( "Set Start", "start", () =>
{
Session.LoopTimeRange = Session.LoopTimeRange is not { } range || range.End <= time
? (time, Session.Project.Duration)
: (time, range.End);
} ).Enabled = Session.LoopTimeRange is null || Session.LoopTimeRange.Value.End > time;
menu.AddOption( "Set End", "last_page", () =>
{
Session.LoopTimeRange = Session.LoopTimeRange is not { } range || range.Start >= time
? (0d, time)
: (range.Start, time);
} ).Enabled = Session.LoopTimeRange is null || Session.LoopTimeRange.Value.Start < time;
menu.AddOption( "Clear", "clear", () =>
{
Session.LoopTimeRange = null;
} ).Enabled = Session.LoopTimeRange is not null;
menu.OpenAtCursor();
}
private (KeyboardModifiers KeyboardModifiers, Vector2 ScenePos) _lastScrub;
protected override void OnMouseMove( GraphicsMouseEvent e )
{
if ( !e.LeftMouseButton ) return;
Scrub( e.KeyboardModifiers, ToScene( e.LocalPosition ) );
}
public void Scrub( KeyboardModifiers modifiers, Vector2 scenePos )
{
_lastScrub = (modifiers, scenePos);
OnScrubUpdate();
}
private void OnScrubUpdate()
{
var (modifiers, scenePos) = _lastScrub;
var sceneView = GraphicsView.SceneRect;
if ( scenePos.x > sceneView.Right )
{
_panSpeed = (scenePos.x - sceneView.Right) * 5f;
scenePos.x = sceneView.Right;
}
else if ( scenePos.x < sceneView.Left )
{
_panSpeed = (scenePos.x - sceneView.Left) * 5f;
scenePos.x = sceneView.Left;
}
else
{
_panSpeed = 0f;
}
var time = Session.ScenePositionToTime( scenePos, new SnapOptions( SnapFlag.Playhead ) );
if ( (modifiers & KeyboardModifiers.Alt) != 0 )
{
if ( time != _dragStartTime )
{
// Alt+Click+Drag: set loop time range
Session.LoopTimeRange = new MovieTimeRange(
MovieTime.Min( time, _dragStartTime ),
MovieTime.Max( time, _dragStartTime ) );
}
else
{
// Alt+Click: clear loop time range
Session.LoopTimeRange = null;
}
}
else
{
// Click: set playhead time
Session.PlayheadTime = time;
}
Update();
}
protected override void OnPaint()
{
var duration = Session.Project.Duration;
Paint.SetBrushAndPen( Timeline.Colors.ChannelBackground );
Paint.DrawRect( LocalRect );
// Darker background for the clip duration
if ( Session.SequenceTimeRange is { } sequenceRange )
{
Paint.SetBrushAndPen( Theme.ControlBackground.LerpTo( Timeline.Colors.Background, 0.5f ) );
DrawTimeRangeRect( (MovieTime.Zero, duration) );
Paint.SetBrushAndPen( Timeline.Colors.Background );
DrawTimeRangeRect( sequenceRange );
}
else
{
Paint.SetBrushAndPen( Timeline.Colors.Background );
DrawTimeRangeRect( (MovieTime.Zero, duration) );
}
// Paste time range
if ( Session.EditMode?.SourceTimeRange is { } pasteRange )
{
var startX = FromScene( Session.TimeToPixels( pasteRange.Start ) ).x;
var endX = FromScene( Session.TimeToPixels( pasteRange.End ) ).x;
var rect = new Rect( new Vector2( startX, LocalRect.Top ), new Vector2( endX - startX, LocalRect.Height ) );
Paint.SetBrushAndPen( Color.White.WithAlpha( 0.2f ) );
Paint.DrawRect( rect );
Paint.PenSize = 1;
Paint.Pen = Color.White.WithAlpha( 0.5f );
Paint.DrawLine( rect.TopLeft, rect.BottomLeft );
Paint.DrawLine( rect.TopRight, rect.BottomRight );
Paint.DrawIcon( rect, "content_paste", 16f );
}
// Loop time range
if ( Session.LoopTimeRange is { } loopRange )
{
var startX = FromScene( Session.TimeToPixels( loopRange.Start ) ).x;
var endX = FromScene( Session.TimeToPixels( loopRange.End ) ).x;
var rect = new Rect( new Vector2( startX, LocalRect.Top ), new Vector2( endX - startX, LocalRect.Height ) );
Paint.SetBrushAndPen( Color.White.WithAlpha( 0.05f ) );
Paint.DrawRect( rect );
Paint.ClearBrush();
Paint.SetPen( Color.White );
Paint.DrawLine( rect.TopLeft, rect.BottomLeft );
Paint.DrawLine( rect.TopRight, rect.BottomRight );
var top = IsTop ? rect.Bottom : rect.Top;
var up = IsTop ? new Vector2( 0f, -1f ) : new Vector2( 0f, 1f );
var leftCorner = new Vector2( rect.Left, top );
var rightCorner = new Vector2( rect.Right, top );
Paint.SetBrush( Color.White.WithAlpha( 0.5f ) );
Paint.DrawPolygon( leftCorner, leftCorner + up * 6f, leftCorner + new Vector2( 6f, 0f ) );
Paint.DrawPolygon( rightCorner, rightCorner + up * 6f, rightCorner - new Vector2( 6f, 0f ) );
}
var range = Session.VisibleTimeRange;
Paint.PenSize = 2;
Paint.Pen = Color.White.WithAlpha( 0.1f );
if ( IsTop )
{
Paint.DrawLine( LocalRect.BottomLeft, LocalRect.BottomRight );
}
else
{
Paint.DrawLine( LocalRect.TopLeft, LocalRect.TopRight );
}
Paint.Antialiasing = true;
Paint.SetFont( "Roboto", 8, 300 );
foreach ( var (style, interval) in Session.Ticks )
{
var height = Height;
var margin = 2f;
switch ( style )
{
case TickStyle.TimeLabel:
Paint.SetPen( Theme.Green.WithAlpha( 0.2f ) );
height -= 12f;
margin = 10f;
break;
case TickStyle.Major:
Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
height -= 6f;
break;
case TickStyle.Minor:
Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
height = 6f;
break;
}
var y = IsTop ? Height - height - margin : margin;
var t0 = MovieTime.Max( (range.Start - interval).Round( interval ), MovieTime.Zero );
var t1 = t0 + range.Duration + interval;
for ( var t = t0; t <= t1; t += interval )
{
var x = FromScene( Session.TimeToPixels( t ) ).x;
if ( style == TickStyle.TimeLabel )
{
var time = Session.PixelsToTime( ToScene( x ).x );
Paint.SetPen( Theme.Green.WithAlpha( 0.2f ) );
Paint.DrawText( new Vector2( x + 6, y ), TimeToString( time, interval ) );
}
else
{
Paint.DrawLine( new Vector2( x, y ), new Vector2( x, y + height ) );
}
}
}
}
private void DrawTimeRangeRect( MovieTimeRange timeRange )
{
var startX = FromScene( Session.TimeToPixels( timeRange.Start ) ).x;
var endX = FromScene( Session.TimeToPixels( timeRange.End ) ).x;
Paint.DrawRect( new Rect( new Vector2( startX, LocalRect.Top ), new Vector2( endX - startX, LocalRect.Height ) ) );
}
private static string TimeToString( MovieTime time, MovieTime interval )
{
return time.ToString();
}
}