From 121ee2f1eeb754bdee07d3f885b89aacd8e3dc43 Mon Sep 17 00:00:00 2001 From: ryan-mangeno Date: Sun, 28 Jun 2026 20:21:43 -0700 Subject: [PATCH 1/3] world saves load but cant save yet --- src/TEdit.Common/TEdit.Common.csproj | 2 +- src/TEdit.Terraria/TEdit.Terraria.csproj | 2 +- src/TEdit5/Controls/SkiaMinimapRenderBox.cs | 7 +- src/TEdit5/Controls/SkiaWorldRenderBox.cs | 13 +- .../Layers/NoSkiaCustomDrawOp.cs | 6 +- src/TEdit5/Program.cs | 75 +++++++----- src/TEdit5/TEdit5.csproj | 112 +++++++++--------- 7 files changed, 113 insertions(+), 104 deletions(-) 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..6ab39671b 100644 --- a/src/TEdit5/Program.cs +++ b/src/TEdit5/Program.cs @@ -1,31 +1,44 @@ -using Avalonia; -using ReactiveUI.Avalonia; -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); - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - { - IconProvider.Current - //.Register() - .Register(); - - return AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace() - .UseReactiveUI(builder => { }); - } -} + +using ReactiveUI.Builder; + + +using Avalonia; +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) + { + ReactiveUI.Builder.RxAppBuilder.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + IconProvider.Current + //.Register() + .Register(); + + + + + + return AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + } +} diff --git a/src/TEdit5/TEdit5.csproj b/src/TEdit5/TEdit5.csproj index 9c6e416d5..147ce48b7 100644 --- a/src/TEdit5/TEdit5.csproj +++ b/src/TEdit5/TEdit5.csproj @@ -1,58 +1,54 @@ - - - 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 + + + + + + + + + + + From d83c5d0aebb0912ffafc340a014ebf50654880d1 Mon Sep 17 00:00:00 2001 From: ryan-mangeno Date: Sun, 28 Jun 2026 21:10:27 -0700 Subject: [PATCH 2/3] saving wiring --- src/TEdit5/Program.cs | 77 ++++++++----------- src/TEdit5/Services/IDialogService.cs | 18 ++++- src/TEdit5/ViewModels/DocumentViewModel.cs | 37 +++++++-- .../ViewModels/ToolSelectionViewModel.cs | 18 +++-- 4 files changed, 92 insertions(+), 58 deletions(-) diff --git a/src/TEdit5/Program.cs b/src/TEdit5/Program.cs index 6ab39671b..8ab8d2a0b 100644 --- a/src/TEdit5/Program.cs +++ b/src/TEdit5/Program.cs @@ -1,44 +1,33 @@ - -using ReactiveUI.Builder; - - -using Avalonia; -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) - { - ReactiveUI.Builder.RxAppBuilder.CreateReactiveUIBuilder() - .WithCoreServices() - .BuildApp(); - - BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); - } - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - { - IconProvider.Current - //.Register() - .Register(); - - - - - - return AppBuilder.Configure() - .UsePlatformDetect() - .WithInterFont() - .LogToTrace(); - } -} +using System; +using Avalonia; +using ReactiveUI.Builder; +using Projektanker.Icons.Avalonia; +using Projektanker.Icons.Avalonia.MaterialDesign; + +namespace TEdit5; + +class Program +{ + [STAThread] + public static void Main(string[] args) + { + RxAppBuilder.CreateReactiveUIBuilder() + .WithCoreServices() + .BuildApp(); + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() + { + IconProvider.Current + //.Register() + .Register(); + + return AppBuilder.Configure() + .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 { }); + _worldEditor = new WorldEditor( + tilePicker, + new TEdit.Editor.TileMaskSettings(), + World, + Selection, + undoManager, + (x, y, height, width) => { }); + } + + public async Task SaveAsync() + { + await World.SaveAsync( + World, + FileName + ); } } 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; From da0ac344ace13a2b2353946d72cadd50e7333408 Mon Sep 17 00:00:00 2001 From: ryan-mangeno Date: Thu, 2 Jul 2026 21:49:13 -0700 Subject: [PATCH 3/3] fixed threading issue and added save button, saving seems to work --- src/TEdit5/ViewModels/DocumentViewModel.cs | 14 +++++++++----- src/TEdit5/Views/MainWindow.axaml | 6 +++++- src/TEdit5/Views/MainWindow.axaml.cs | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/TEdit5/ViewModels/DocumentViewModel.cs b/src/TEdit5/ViewModels/DocumentViewModel.cs index c93522cf1..37218c27d 100644 --- a/src/TEdit5/ViewModels/DocumentViewModel.cs +++ b/src/TEdit5/ViewModels/DocumentViewModel.cs @@ -48,11 +48,15 @@ public DocumentViewModel( (x, y, height, width) => { }); } - public async Task SaveAsync() + public Task SaveAsync() { - await World.SaveAsync( - World, - FileName - ); + // 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/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