📄 ui/src/Views/MainWindow.axaml.cs
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Cfa.Ui.ViewModels;
using Nodify;

namespace Cfa.Ui.Views;

public sealed partial class MainWindow : Window
{
    private MainViewModel? model;

    public MainWindow()
    {
        AvaloniaXamlLoader.Load(this);
        DataContextChanged += OnDataContextChanged;
        Closed += (_, _) =>
        {
            if (model is not null)
            {
                model.PropertyChanged -= OnModelChanged;
            }
        };
    }

    private void OnDataContextChanged(object? sender, EventArgs args)
    {
        if (model is not null)
        {
            model.PropertyChanged -= OnModelChanged;
        }
        model = DataContext as MainViewModel;
        if (model is not null)
        {
            model.PropertyChanged += OnModelChanged;
        }
    }

    private void OnModelChanged(object? sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == nameof(MainViewModel.Scene))
        {
            Dispatcher.UIThread.Post(() => FitGraph(this, new RoutedEventArgs()), DispatcherPriority.Loaded);
        }
    }

    private void FitGraph(object? sender, RoutedEventArgs args)
    {
        var editor = this.FindControl<NodifyEditor>("Editor")!;
        editor.UpdateLayout();
        if (model?.Scene is { } scene)
        {
            if (scene.Nodes.Count > 0 && editor.Bounds.Width > 0 && editor.Bounds.Height > 0)
            {
                var bounds = scene.Nodes.Select(node => node.Bounds).Aggregate((first, next) => first.Union(next));
                var zoom = Math.Min(
                    editor.MaxViewportZoom,
                    Math.Min(editor.Bounds.Width / bounds.Width, editor.Bounds.Height / bounds.Height)
                );
                editor.MinViewportZoom = Math.Min(editor.MinViewportZoom, zoom);
                editor.ViewportZoom = zoom;
                editor.ViewportLocation =
                    bounds.Center
                    - new Point(
                        editor.Bounds.Width / editor.ViewportZoom / 2,
                        editor.Bounds.Height / editor.ViewportZoom / 2
                    );
            }
        }
    }
}