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