diff --git a/src/TEdit.Common/TEdit.Common.csproj b/src/TEdit.Common/TEdit.Common.csproj
index 092ce141b..3a55fc0d8 100644
--- a/src/TEdit.Common/TEdit.Common.csproj
+++ b/src/TEdit.Common/TEdit.Common.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/TEdit.Terraria/TEdit.Terraria.csproj b/src/TEdit.Terraria/TEdit.Terraria.csproj
index 00ab6b42b..aa0d8fc63 100644
--- a/src/TEdit.Terraria/TEdit.Terraria.csproj
+++ b/src/TEdit.Terraria/TEdit.Terraria.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/TEdit5/Controls/SkiaMinimapRenderBox.cs b/src/TEdit5/Controls/SkiaMinimapRenderBox.cs
index a0e033d3c..85391d7b1 100644
--- a/src/TEdit5/Controls/SkiaMinimapRenderBox.cs
+++ b/src/TEdit5/Controls/SkiaMinimapRenderBox.cs
@@ -12,7 +12,7 @@ namespace TEdit5.Controls;
public class SkiaMinimapRenderBox : Control
{
- private readonly GlyphRun _noSkia;
+ private readonly GlyphRun? _noSkia;
static SkiaMinimapRenderBox()
{
@@ -27,10 +27,7 @@ public SkiaMinimapRenderBox()
RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.None);
ClipToBounds = true;
- // "No Skia" text for unsupported platforms
- var text = "Current rendering API is not Skia";
- var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
- _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
+ // ponytail: _noSkia only renders on non-Skia backends
}
public override void Render(DrawingContext context)
diff --git a/src/TEdit5/Controls/SkiaWorldRenderBox.cs b/src/TEdit5/Controls/SkiaWorldRenderBox.cs
index 771884339..7eca04ee1 100644
--- a/src/TEdit5/Controls/SkiaWorldRenderBox.cs
+++ b/src/TEdit5/Controls/SkiaWorldRenderBox.cs
@@ -427,7 +427,7 @@ public enum SelectionModes
#endregion
- private readonly GlyphRun _noSkia;
+ private readonly GlyphRun? _noSkia;
ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default;
private Point _startMousePosition;
private Vector _startScrollPosition;
@@ -452,10 +452,7 @@ public SkiaWorldRenderBox()
RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.None);
ClipToBounds = true;
- // "No Skia" text for unsupported platforms
- var text = "Current rendering API is not Skia";
- var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
- _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
+ // ponytail: _noSkia only renders on non-Skia backends, which macOS never uses
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -938,6 +935,12 @@ public Rect GetImageViewPort()
protected internal ScrollBar HorizontalScrollBar = null!;
protected internal ScrollBar VerticalScrollBar = null!;
+ ///
+ public bool CanHorizontallyScroll { get; set; } = true;
+
+ ///
+ public bool CanVerticallyScroll { get; set; } = true;
+
///
public Size Extent => new(
Math.Max(ViewPort.Bounds.Width, World?.TilesWide ?? 0 * (Zoom / 100d)),
diff --git a/src/TEdit5/Controls/WorldRenderEngine/Layers/NoSkiaCustomDrawOp.cs b/src/TEdit5/Controls/WorldRenderEngine/Layers/NoSkiaCustomDrawOp.cs
index 0867a8da7..63ac4df56 100644
--- a/src/TEdit5/Controls/WorldRenderEngine/Layers/NoSkiaCustomDrawOp.cs
+++ b/src/TEdit5/Controls/WorldRenderEngine/Layers/NoSkiaCustomDrawOp.cs
@@ -8,12 +8,12 @@ namespace TEdit5.Controls.WorldRenderEngine.Layers;
public class NoSkiaCustomDrawOp : ICustomDrawOperation
{
- private readonly IImmutableGlyphRunReference _noSkia;
+ private readonly IImmutableGlyphRunReference? _noSkia;
- public NoSkiaCustomDrawOp(Rect bounds, GlyphRun noSkia)
+ public NoSkiaCustomDrawOp(Rect bounds, GlyphRun? noSkia)
{
Bounds = bounds;
- _noSkia = noSkia.TryCreateImmutableGlyphRunReference();
+ _noSkia = noSkia?.TryCreateImmutableGlyphRunReference();
}
public void Dispose()
diff --git a/src/TEdit5/Program.cs b/src/TEdit5/Program.cs
index 6c41c3677..8ab8d2a0b 100644
--- a/src/TEdit5/Program.cs
+++ b/src/TEdit5/Program.cs
@@ -1,21 +1,24 @@
-using Avalonia;
-using ReactiveUI.Avalonia;
+using System;
+using Avalonia;
+using ReactiveUI.Builder;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.MaterialDesign;
-using System;
namespace TEdit5;
class Program
{
- // Initialization code. Don't use any Avalonia, third-party APIs or any
- // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
- // yet and stuff might break.
[STAThread]
- public static void Main(string[] args) => BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
+ public static void Main(string[] args)
+ {
+ RxAppBuilder.CreateReactiveUIBuilder()
+ .WithCoreServices()
+ .BuildApp();
+
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
- // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
IconProvider.Current
@@ -23,9 +26,8 @@ public static AppBuilder BuildAvaloniaApp()
.Register();
return AppBuilder.Configure()
- .UsePlatformDetect()
- .WithInterFont()
- .LogToTrace()
- .UseReactiveUI(builder => { });
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
}
}
diff --git a/src/TEdit5/Services/IDialogService.cs b/src/TEdit5/Services/IDialogService.cs
index 3fceb3db4..74e0f3a30 100644
--- a/src/TEdit5/Services/IDialogService.cs
+++ b/src/TEdit5/Services/IDialogService.cs
@@ -24,9 +24,13 @@ public class DialogService : IDialogService
public interface IDocumentService
{
ObservableCollection Documents { get; }
- Task LoadWorldAsync(IStorageFile file, IProgress? progress = null);
-}
+ Task LoadWorldAsync(
+ IStorageFile file,
+ IProgress? progress = null);
+
+ Task SaveAsync(DocumentViewModel document);
+}
public partial class DocumentService : ReactiveObject, IDocumentService
{
@@ -51,7 +55,11 @@ public async Task LoadWorldAsync(IStorageFile file, IProgress
-
- WinExe
- net10.0
- latest
- enable
- true
- app.manifest
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- DocumentView.axaml
-
-
- PaintModeView.axaml
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Exe
+ net10.0
+ latest
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DocumentView.axaml
+
+
+ PaintModeView.axaml
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TEdit5/ViewModels/DocumentViewModel.cs b/src/TEdit5/ViewModels/DocumentViewModel.cs
index df396f845..37218c27d 100644
--- a/src/TEdit5/ViewModels/DocumentViewModel.cs
+++ b/src/TEdit5/ViewModels/DocumentViewModel.cs
@@ -1,9 +1,9 @@
-using Avalonia;
+using Avalonia;
using TEdit5.Controls;
using TEdit.Editor;
using TEdit.Editor.Undo;
using TEdit.Terraria;
-
+using System.Threading.Tasks;
namespace TEdit5.ViewModels;
public partial class DocumentViewModel : ReactiveObject
@@ -14,20 +14,49 @@ public partial class DocumentViewModel : ReactiveObject
[Reactive] private Point _cursorTileCoordinate;
[Reactive] private SkiaWorldRenderBox.SelectionModes _selectionMode;
+ [Reactive] private World _world;
+ [Reactive] private WorldEditor _worldEditor;
+
+ public string FileName { get; }
+
public ToolSelectionViewModel ToolSelection { get; }
public TilePicker TilePicker { get; }
public ISelection Selection { get; }
- [Reactive] private World _world;
- [Reactive] private WorldEditor _worldEditor;
- public DocumentViewModel(World world, ToolSelectionViewModel toolSelection, TilePicker tilePicker)
+ public DocumentViewModel(
+ World world,
+ string fileName,
+ ToolSelectionViewModel toolSelection,
+ TilePicker tilePicker)
{
_world = world;
+ FileName = fileName;
+
ToolSelection = toolSelection;
TilePicker = tilePicker;
+
Selection = new Selection();
+
IUndoManager undoManager = null;
- _worldEditor = new WorldEditor(tilePicker, new TEdit.Editor.TileMaskSettings(), World, Selection, undoManager, (x, y, height, width) => { });
+ _worldEditor = new WorldEditor(
+ tilePicker,
+ new TEdit.Editor.TileMaskSettings(),
+ World,
+ Selection,
+ undoManager,
+ (x, y, height, width) => { });
+ }
+
+ public Task SaveAsync()
+ {
+ // Run synchronously on the UI thread. Saving mutates World state that is
+ // bound to the UI (the PropertyGrid over World, plus the Chests/Signs/
+ // TileEntities ObservableCollections that Validate() prunes, and
+ // FileRevision). World.SaveAsync does this work on a background thread,
+ // so those change notifications reach Avalonia bindings off-thread and
+ // throw "Call from invalid thread". World.Save is the synchronous variant.
+ World.Save(World, FileName);
+ return Task.CompletedTask;
}
}
diff --git a/src/TEdit5/ViewModels/ToolSelectionViewModel.cs b/src/TEdit5/ViewModels/ToolSelectionViewModel.cs
index 691dcacfe..d8efcc4a4 100644
--- a/src/TEdit5/ViewModels/ToolSelectionViewModel.cs
+++ b/src/TEdit5/ViewModels/ToolSelectionViewModel.cs
@@ -1,6 +1,9 @@
-using System.Collections.Generic;
+using Avalonia.Threading;
+using ReactiveUI;
+using System.Collections.Generic;
using System.Linq;
using System.Reactive;
+using System.Reactive.Concurrency;
using TEdit5.Editor;
namespace TEdit5.ViewModels;
@@ -17,17 +20,22 @@ public partial class ToolSelectionViewModel : ReactiveObject
public ToolSelectionViewModel(IEnumerable tools)
{
_tools = tools.ToList();
- SetToolCommand = ReactiveCommand.Create(SetTool);
+
+ SetToolCommand = ReactiveCommand.Create(
+ tool => SetTool(tool),
+ outputScheduler: CurrentThreadScheduler.Instance
+ );
SetTool(Tools.FirstOrDefault(t => t.Name == "Arrow"));
}
private void SetTool(IMouseTool? tool)
{
- // deactivate previous tool
- if (ActiveTool != null) { ActiveTool.IsActive = false; }
+ if (ActiveTool != null)
+ {
+ ActiveTool.IsActive = false;
+ }
- // activate new tool
if (tool != null)
{
tool.IsActive = true;
diff --git a/src/TEdit5/Views/MainWindow.axaml b/src/TEdit5/Views/MainWindow.axaml
index daf4e79dc..b51f5a650 100644
--- a/src/TEdit5/Views/MainWindow.axaml
+++ b/src/TEdit5/Views/MainWindow.axaml
@@ -33,7 +33,11 @@
-
+
+
+
+
diff --git a/src/TEdit5/Views/MainWindow.axaml.cs b/src/TEdit5/Views/MainWindow.axaml.cs
index 5edca7254..6c1fc5ebd 100644
--- a/src/TEdit5/Views/MainWindow.axaml.cs
+++ b/src/TEdit5/Views/MainWindow.axaml.cs
@@ -23,6 +23,24 @@ public async void LoadWorldButton_Clicked(object sender, RoutedEventArgs args)
await OpenWorldDialog();
}
+ public async void SaveWorldButton_Clicked(object sender, RoutedEventArgs args)
+ {
+ var document = MainWindowViewModel.SelectedDocument;
+ if (document == null) return;
+
+ MainWindowViewModel.ProgressText = "Saving...";
+ try
+ {
+ await MainWindowViewModel.DocumentService.SaveAsync(document);
+ MainWindowViewModel.ProgressText = "Saved";
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("SAVE FAILED: " + ex);
+ MainWindowViewModel.ProgressText = "Save failed: " + ex.Message;
+ }
+ }
+
private async Task OpenWorldDialog()
{
var fileTypes = new List