Improve Win2D editor UX
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user