using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using Artemis.UI.Shared; using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.PanAndZoom; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; using Avalonia.Threading; using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptView : ReactiveUserControl { private readonly Grid _grid; private readonly ItemsControl _nodesContainer; private readonly SelectionRectangle _selectionRectangle; private readonly ZoomBorder _zoomBorder; public NodeScriptView() { InitializeComponent(); _grid = this.Find("ContainerGrid"); _zoomBorder = this.Find("ZoomBorder"); _nodesContainer = this.Find("NodesContainer"); _selectionRectangle = this.Find("SelectionRectangle"); _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; UpdateZoomBorderBackground(); _zoomBorder.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); _zoomBorder.AddHandler(PointerWheelChangedEvent, ZoomOnPointerWheelChanged, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); this.WhenActivated(d => { ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested; ViewModel.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); if (ViewModel.IsPreview) { BoundsProperty.Changed.Subscribe(BoundsPropertyChanged).DisposeWith(d); ViewModel.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d); } Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle); Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); }); } protected override Size MeasureOverride(Size availableSize) { AutoFitIfPreview(); return base.MeasureOverride(availableSize); } private void ZoomOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { // If scroll events aren't handled here the ZoomBorder does some random panning when at the zoom limit if (e.Delta.Y > 0 && _zoomBorder.ZoomX >= 1) e.Handled = true; } private void ShowPickerAt(Point point) { if (ViewModel == null) return; ViewModel.NodePickerViewModel.Position = point; _zoomBorder?.ContextFlyout?.ShowAt(_zoomBorder, true); } private void AutoFitIfPreview() { if (ViewModel != null && ViewModel.IsPreview) AutoFit(true); } private void BoundsPropertyChanged(AvaloniaPropertyChangedEventArgs obj) { if (_nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl).Contains(obj.Sender)) AutoFitIfPreview(); } private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) { // If the flyout handled the click, update the position of the node picker if (e.Handled && ViewModel != null) ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid); } private void AutoFit(bool skipTransitions) { if (!_nodesContainer.ItemContainerGenerator.Containers.Any()) return; double left = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Left).Min(); double top = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Top).Min(); double bottom = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Bottom).Max(); double right = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Right).Max(); // Add a 10 pixel margin around the rect Rect scriptRect = new(new Point(left - 10, top - 10), new Point(right + 10, bottom + 10)); // The scale depends on the available space double scale = Math.Min(1, Math.Min(Bounds.Width / scriptRect.Width, Bounds.Height / scriptRect.Height)); // Pan and zoom to make the script fit _zoomBorder.Zoom(scale, 0, 0, skipTransitions); _zoomBorder.Pan(Bounds.Center.X - scriptRect.Center.X * scale, Bounds.Center.Y - scriptRect.Center.Y * scale, skipTransitions); } private void ViewModelOnAutoFitRequested(object? sender, EventArgs e) { Dispatcher.UIThread.Post(() => AutoFit(false)); } private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property.Name == nameof(_zoomBorder.Background)) UpdateZoomBorderBackground(); } private void UpdateZoomBorderBackground() { if (_zoomBorder.Background is VisualBrush visualBrush) visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) { if (ViewModel != null) ViewModel.PanMatrix = _zoomBorder.Matrix; UpdateZoomBorderBackground(); } private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) { List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); } private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) { ViewModel?.FinishNodeSelection(); } private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!_selectionRectangle.IsSelecting && e.InitialPressMouseButton == MouseButton.Left) ViewModel?.ClearNodeSelection(); } }