Improve Win2D editor UX

This commit is contained in:
2026-05-09 02:41:05 +02:00
parent 6e8766db3f
commit e90609bcee
6 changed files with 191 additions and 28 deletions

View File

@@ -2,6 +2,7 @@
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
@@ -11,7 +12,9 @@ using System.Globalization;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.System;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Popups;
using WinRT.Interop;
@@ -34,16 +37,21 @@ public sealed partial class MainWindow
private sealed record ForecastViewModel(BitmapImage Icon, string Message);
private sealed record EditorToolViewModel(EEditorTool Tool, BitmapImage? Icon, string Label);
private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label)
{
public EEditorTool Tool { get; } = tool;
public BitmapImage? Icon { get; } = icon;
public string Label { get; } = label;
public bool IsSelected { get; set; }
}
public MainWindow()
{
InitializeComponent();
m_Level = BuildStarterLevel();
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString())).ToArray();
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
ToolPicker.ItemsSource = m_EditorTools;
ToolPicker.SelectedItem = m_EditorTools.First(tool => tool.Tool == m_SelectedTool);
RefreshInspector();
}
@@ -78,10 +86,14 @@ public sealed partial class MainWindow
return await CanvasBitmap.LoadAsync(sender, path);
}
private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
private void ToolRadio_Checked(object sender, RoutedEventArgs e)
{
if (ToolPicker.SelectedItem is EditorToolViewModel tool)
if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
{
m_SelectedTool = tool.Tool;
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
}
}
private void New_Click(object sender, RoutedEventArgs e)
@@ -163,21 +175,123 @@ public sealed partial class MainWindow
private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
{
m_Painting = true;
_ = LevelCanvas.CapturePointer(e.Pointer);
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
var point = e.GetCurrentPoint(LevelCanvas);
if (point.Properties.IsLeftButtonPressed && IsShiftDown())
{
m_Panning = true;
m_LastPanPoint = point.Position;
_ = LevelCanvas.CapturePointer(e.Pointer);
e.Handled = true;
return;
}
if (point.Properties.IsRightButtonPressed)
{
RemovePropAt(point.Position);
e.Handled = true;
return;
}
if (point.Properties.IsLeftButtonPressed)
{
_ = LevelCanvas.CapturePointer(e.Pointer);
SelectOrPaintAt(point.Position);
m_Painting = m_SelectedTool != EEditorTool.Cursor;
e.Handled = true;
}
}
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
if (m_Panning)
{
var deltaX = point.Position.X - m_LastPanPoint.X;
var deltaY = point.Position.Y - m_LastPanPoint.Y;
m_LastPanPoint = point.Position;
m_PanX += deltaX;
m_PanY += deltaY;
ClampPan();
LevelCanvas.Invalidate();
e.Handled = true;
return;
}
if (m_Painting)
PaintAt(e.GetCurrentPoint(LevelCanvas).Position);
{
PaintAt(point.Position);
e.Handled = true;
}
}
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
m_Painting = false;
m_Panning = false;
LevelCanvas.ReleasePointerCapture(e.Pointer);
e.Handled = true;
}
private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
var wheelDelta = point.Properties.MouseWheelDelta;
if (wheelDelta == 0)
return;
ZoomAt(point.Position, wheelDelta > 0 ? c_ZoomStep : 1 / c_ZoomStep);
e.Handled = true;
}
private static bool IsShiftDown()
{
return InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
}
private void ZoomAt(Point point, double zoomFactor)
{
var oldLayout = GetLayout();
var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
var newCellSize = GetBaseCellSize() * m_Zoom;
var originWithoutPan = GetCenteredOrigin(newCellSize);
m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize);
m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize);
ClampPan();
LevelCanvas.Invalidate();
}
private void SelectOrPaintAt(Point point)
{
if (m_SelectedTool == EEditorTool.Cursor)
SelectAt(point);
else
PaintAt(point);
}
private void SelectAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
m_SelectedCell = position;
RefreshInspector();
LevelCanvas.Invalidate();
}
private void RemovePropAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
var cell = m_Level.GetCell(position);
m_SelectedCell = position;
m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None });
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
RefreshInspector();
LevelCanvas.Invalidate();
}
private void PaintAt(Point point)
@@ -417,14 +531,47 @@ public sealed partial class MainWindow
}
private CanvasLayout GetLayout()
{
ClampPan();
var cellSize = GetBaseCellSize() * m_Zoom;
var centeredOrigin = GetCenteredOrigin(cellSize);
return new(cellSize, centeredOrigin.X + m_PanX, centeredOrigin.Y + m_PanY);
}
private double GetBaseCellSize()
{
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height));
size = Math.Max(20, size);
var originX = Math.Max(0, (availableWidth - (size * m_Level.Width)) / 2);
var originY = Math.Max(0, (availableHeight - (size * m_Level.Height)) / 2);
return new(size, originX, originY);
return Math.Max(20, size);
}
private Point GetCenteredOrigin(double cellSize)
{
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2);
}
private void ClampPan()
{
var cellSize = GetBaseCellSize() * m_Zoom;
var contentWidth = cellSize * m_Level.Width;
var contentHeight = cellSize * m_Level.Height;
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth);
m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight);
}
private static double ClampAxisPan(double pan, double contentSize, double availableSize)
{
if (contentSize <= availableSize)
return 0;
var maxPan = (contentSize - availableSize) / 2;
return Math.Clamp(pan, -maxPan, maxPan);
}
private void RefreshInspector()
@@ -465,7 +612,9 @@ public sealed partial class MainWindow
private static BitmapImage? EditorToolIcon(EEditorTool tool)
{
return tool switch {
EEditorTool.Floor or EEditorTool.Wall => null,
EEditorTool.Cursor => PropImage("cursor.png"),
EEditorTool.Floor => PropImage("floor.png"),
EEditorTool.Wall => PropImage("wall.png"),
EEditorTool.Reactor => PropImage("reactor.png"),
EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
EEditorTool.Generator => PropImage("generator.png"),
@@ -563,6 +712,9 @@ public sealed partial class MainWindow
private const int c_EastConnection = 2;
private const int c_SouthConnection = 4;
private const int c_WestConnection = 8;
private const double c_MinZoom = 0.5;
private const double c_MaxZoom = 4;
private const double c_ZoomStep = 1.15;
private readonly SimulationEngine m_Simulation = new();
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
@@ -571,8 +723,13 @@ public sealed partial class MainWindow
private LevelState m_Level;
private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
private bool m_Painting;
private bool m_Panning;
private Point m_LastPanPoint;
private GridPosition? m_SelectedCell;
private EEditorTool m_SelectedTool = EEditorTool.Floor;
private EEditorTool m_SelectedTool = EEditorTool.Cursor;
private double m_Zoom = 1;
private double m_PanX;
private double m_PanY;
private CanvasBitmap? m_TerrainTilemap;
private CanvasBitmap? m_RobotSprite;
private CanvasBitmap? m_LeakSprite;