Editor/Attack/Widgets/TimelineWidget.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace MANIFOLD.BHLib.Editor {
public class TimelineWidget : GraphicsView {
public class EventMarker : GraphicsItem {
public float time;
public List<AttackEvent> events;
public bool selected;
public int selectedIndex;
public Action<EventMarker> onSelected;
private TimelineWidget timeline;
public EventMarker(TimelineWidget widget) {
timeline = widget;
events = new List<AttackEvent>();
ZIndex = 0;
HoverEvents = true;
}
protected override void OnMousePressed(GraphicsMouseEvent e) {
if (e.LeftMouseButton) {
if (!selected) selectedIndex = 0;
else {
selectedIndex++;
if (selectedIndex >= events.Count) selectedIndex = 0;
}
selected = true;
Update();
onSelected?.Invoke(this);
e.Accepted = true;
}
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Color unselectedColor = Color.White.Darken(0.05f);
Color color;
if (selected) color = Color.Orange;
else if (Paint.HasMouseOver) color = Gizmo.Colors.Hovered;
else color = unselectedColor;
Paint.SetPen(color, 2);
var rect = LocalRect.Shrink(2);
Paint.DrawLine(new Vector2(rect.Left, rect.Bottom), new Vector2(rect.Left, rect.Top));
// Paint.DrawText(rect.Shrink(4, 0, 0, 6), string.Join("\n", events.Select(x => x.Name)), TextFlag.LeftBottom);
rect = rect.Shrink(4, 0, 0, 6);
for (int i = 0; i < events.Count; i++) {
Color textColor;
if (selected) {
textColor = i == selectedIndex ? color : unselectedColor;
} else {
textColor = color;
}
Paint.SetPen(textColor);
Paint.DrawText(rect, events[i].Name, TextFlag.LeftBottom);
rect = rect.Shrink(0, 0, 0, 9);
}
}
}
public class TimeAxis : GraphicsItem {
private TimelineWidget timeline;
public TimeAxis(TimelineWidget widget) {
timeline = widget;
ZIndex = 10;
HoverEvents = true;
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Paint.ClearPen();
Paint.SetBrush(Theme.ControlBackground);
Paint.DrawRect(LocalRect);
Paint.SetDefaultFont(7);
var rect = LocalRect.Shrink(1);
var zoom = timeline.ZoomFactor;
var spacing = 100 * zoom;
var lines = rect.Width / spacing;
var w = spacing;
var subdivisions = (int)(3 * zoom);
var subLineSpacing = w / subdivisions;
for (int i = 0; i < lines; i++) {
float xPos = rect.Left + w * i;
Paint.SetPen(Theme.Text.WithAlpha(0.5f));
Paint.DrawLine(new Vector2(xPos, rect.Bottom), new Vector2(xPos, rect.Bottom - 8));
Paint.DrawText(new Vector2(xPos, rect.Top), $"{i}");
Paint.SetPen(Theme.Text.WithAlpha(0.2f));
for (int j = 0; j < subdivisions; j++) {
var sublineX = w * i + subLineSpacing * j;
Paint.DrawLine(new Vector2(rect.Left + sublineX, rect.Bottom), new Vector2(rect.Left + sublineX, rect.Bottom - 4));
}
}
}
protected override void OnMousePressed(GraphicsMouseEvent e) {
base.OnMousePressed(e);
if (e.LeftMouseButton) {
timeline.ScrubTo(timeline.TimeFromPosition(e.LocalPosition.x));
}
}
}
public class Scrubber : GraphicsItem {
private TimelineWidget timeline;
public Scrubber(TimelineWidget widget) {
timeline = widget;
ZIndex = 20;
HoverEvents = true;
Cursor = CursorShape.SizeH;
Movable = true;
Selectable = true;
}
protected override void OnPaint() {
base.OnPaint();
Paint.Antialiasing = false;
Paint.ClearPen();
Paint.SetBrush(Theme.Green.WithAlpha(0.7f));
Paint.DrawRect(new Rect(0, new Vector2(LocalRect.Width, Theme.RowHeight + 1)));
Paint.SetPen(Theme.Green.WithAlpha(0.7f));
Paint.DrawLine(new Vector2(4, Theme.RowHeight + 1), new Vector2(4, LocalRect.Bottom));
}
protected override void OnMoved() {
base.OnMoved();
timeline.ScrubTo(timeline.TimeFromPosition(Position.x));
Position = Position.WithY(0);
Position = Position.WithX(MathF.Max(-4, Position.x));
}
}
private ITimeline timeline;
private float time;
private float zoomFactor;
private bool showHidden;
private TimeAxis timeAxis;
private Scrubber scrubber;
private List<EventMarker> markers;
private Dictionary<AttackEvent, EventMarker> eventToMarker;
private EventMarker selectedMarker;
public ITimeline Timeline {
get => timeline;
set {
timeline = value;
RebuildMarkers();
DoLayout();
}
}
public float Range { get; set; }
public float ZoomFactor {
get => zoomFactor;
set {
zoomFactor = value;
DoLayout();
timeAxis.Update();
scrubber.Update();
}
}
public float Time {
get => time;
set {
time = value;
DoLayout();
}
}
public bool ShowHidden {
get => showHidden;
set {
showHidden = value;
RebuildMarkers();
}
}
public AttackEvent SelectedEvent => selectedMarker?.events[selectedMarker.selectedIndex];
public Action<AttackEvent> OnEventSelected { get; set; }
public Action OnTimeScrubbed { get; set; }
public TimelineWidget(Widget parent = null) : base(parent) {
Antialiasing = false;
BilinearFiltering = false;
SceneRect = new Rect(0, Size);
HorizontalScrollbar = ScrollbarMode.Auto;
VerticalScrollbar = ScrollbarMode.Off;
MouseTracking = true;
Scale = 1;
zoomFactor = 1;
timeAxis = new TimeAxis(this);
Add(timeAxis);
scrubber = new Scrubber(this);
Add(scrubber);
markers = new List<EventMarker>();
eventToMarker = new Dictionary<AttackEvent, EventMarker>();
}
protected override void DoLayout() {
base.DoLayout();
var size = Size;
size.x = MathF.Max(size.x, PositionFromTime(Range + 3));
SceneRect = new Rect(0, size);
timeAxis.Size = new Vector2(size.x, Theme.RowHeight);
scrubber.Size = new Vector2(9, size.y);
var rect = SceneRect;
rect.Top = timeAxis.SceneRect.Bottom;
scrubber.Position = scrubber.Position.WithX(PositionFromTime(Time) - 3).SnapToGrid(1f);
foreach (var marker in markers) {
var markerRect = rect;
markerRect.Left = PositionFromTime(marker.time);
markerRect.Width = 80;
marker.SceneRect = markerRect;
}
}
protected override void OnWheel(WheelEvent e) {
e.Accept();
}
public void RebuildMarkers() {
var previousSelectedEvent = SelectedEvent;
foreach (var marker in markers) {
marker.Destroy();
}
markers.Clear();
eventToMarker.Clear();
if (timeline != null) {
foreach (var evt in Timeline.Events) {
if (!ShowHidden && evt.Hidden) continue;
AddEvent(evt);
}
}
DoLayout();
if (previousSelectedEvent != null) {
if (eventToMarker.TryGetValue(previousSelectedEvent, out EventMarker marker)) {
marker.selected = true;
marker.selectedIndex = marker.events.IndexOf(previousSelectedEvent);
try {
marker.Update();
} catch {
// ignored
}
}
}
}
public void ScrubTo(float time) {
Time = time;
OnTimeScrubbed?.Invoke();
}
public float PositionFromTime(float time) {
return 100 * ZoomFactor * time;
}
public float TimeFromPosition(float position) {
return (ZoomFactor / 100) * position;
}
private void AddEvent(AttackEvent evt) {
var existing = markers.FirstOrDefault(x => x.time.AlmostEqual(evt.Time));
if (existing != null) {
existing.events.Add(evt);
eventToMarker.Add(evt, existing);
} else {
EventMarker marker = new EventMarker(this);
marker.time = evt.Time;
marker.events.Add(evt);
marker.onSelected = OnMarkerSelected;
Add(marker);
markers.Add(marker);
eventToMarker.Add(evt, marker);
}
}
private void OnMarkerSelected(EventMarker marker) {
if (marker != selectedMarker && selectedMarker != null) {
selectedMarker.selected = false;
try {
selectedMarker.Update();
} catch {
// ignored
}
}
selectedMarker = marker;
OnEventSelected?.Invoke(SelectedEvent);
}
}
}