Files
sbox-public/game/editor/MovieMaker/Code/Editor/ScrubBar.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

374 lines
9.0 KiB
C#

using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
/// <summary>
/// A bar with times and notches on it
/// </summary>
public class ScrubBar : GraphicsItem, ISnapSource, IMovieItem
{
[FromTheme]
public static Color TimeLabelColor { get; set; } = Theme.Green.WithAlpha( 0.25f );
[FromTheme]
public static new float Height { get; set; } = 24f;
public MovieEditor Editor { get; }
public Session Session { get; }
public bool IsTop { get; }
MovieTimeRange IMovieItem.TimeRange => (MovieTime.Zero, MovieTime.MaxValue);
bool IMovieItem.OverridesMouseEvents => true;
public ScrubBar( MovieEditor timelineEditor, bool isTop )
{
Session = timelineEditor.Session;
Editor = timelineEditor;
IsTop = isTop;
ZIndex = 450;
HoverEvents = true;
Selectable = true;
base.Height = Height;
}
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.ScrollByImmediate( delta );
Session.DispatchViewChanged();
_lastScrub.ScenePos -= new Vector2( delta, 0f );
OnScrubUpdate();
}
}
protected override void OnMousePressed( GraphicsMouseEvent e )
{
var time = Session.ScenePositionToTime( ToScene( e.LocalPosition ), new SnapOptions( source => source is not TimeCursor ) );
if ( e.MiddleMouseButton )
{
// Panning handled by timeline
return;
}
e.Accepted = true;
if ( e.RightMouseButton )
{
ShowContextMenu( time );
return;
}
StartScrubbing( time, e.KeyboardModifiers );
}
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( source => source is not TimeCursor ) );
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.Duration;
var baseColor = Session.EditMode?.ScrubBarOverrideColor ?? Timeline.BackgroundColor;
Paint.SetBrushAndPen( baseColor );
Paint.DrawRect( LocalRect );
// Darker background for the clip duration
if ( Session.SequenceTimeRange is { } sequenceRange )
{
Paint.SetBrushAndPen( baseColor.TintRelative( Timeline.BackgroundColor, Timeline.OuterColor ) );
DrawTimeRangeRect( (MovieTime.Zero, duration) );
Paint.SetBrushAndPen( baseColor.TintRelative( Timeline.BackgroundColor, Timeline.InnerColor ) );
DrawTimeRangeRect( sequenceRange );
}
else
{
Paint.SetBrushAndPen( baseColor.TintRelative( Timeline.BackgroundColor, Timeline.InnerColor ) );
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;
default:
continue;
}
var y = IsTop ? Height - height - margin : margin;
var t0 = MovieTime.Max( (range.Start - interval).Round( interval ), MovieTime.Zero );
var t1 = t0 + range.Duration + interval * 2;
for ( var t = t0; t <= t1; t += interval )
{
var x = FromScene( Session.TimeToPixels( t ) ).x;
Paint.SetPen( TimeLabelColor );
var labelPos = new Vector2( x + 6, y );
var time = Session.PixelsToTime( ToScene( x ).x );
if ( IsTop )
{
Paint.DrawText( labelPos, TimeToString( time, interval ) );
}
else
{
var frame = time.GetFrameIndex( Session.FrameRate );
Paint.DrawText( labelPos, frame.ToString( "0000" ) );
}
}
}
}
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();
}
public IEnumerable<SnapTarget> GetSnapTargets( MovieTime sourceTime, bool isPrimary )
{
if ( Session.EditMode?.SourceTimeRange is { } sourceRange )
{
yield return sourceRange.Start;
yield return sourceRange.End;
}
if ( Session.LoopTimeRange is { } loopRange )
{
yield return loopRange.Start;
yield return loopRange.End;
}
if ( !isPrimary ) yield break;
yield return new SnapTarget( sourceTime.Round( Session.MinorTick.Interval ), -2 );
yield return new SnapTarget( sourceTime.Round( Session.MajorTick.Interval ), -1 );
}
}