diff --git a/.github/workflows/editorconfig.yml b/.github/workflows/editorconfig.yml index 203cb755..c1283a51 100644 --- a/.github/workflows/editorconfig.yml +++ b/.github/workflows/editorconfig.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 # from https://github.com/marketplace/actions/dotnet-format steps: - name: Get branch info # todo: is this needed? @@ -33,10 +34,11 @@ jobs: working-directory: ./src - name: Inspect code - uses: muno92/resharper_inspectcode@1.4.0 + uses: muno92/resharper_inspectcode@1.12.0 with: solutionPath: ./src/Consolonia.sln - exclude: "**Consolonia.GuiCS/**.*" + exclude: "**Consolonia.GuiCS/**.*;**Consolonia.Gallery/**.*;**Example/**.*" + version: '2024.2.6' - name: install-resharper run: | @@ -45,7 +47,7 @@ jobs: - name: resharper-cleanup run: | - jb cleanupcode Consolonia.sln --exclude="**Consolonia.GuiCS\**.*" + jb cleanupcode Consolonia.sln --exclude="**Consolonia.GuiCS/**.*;**Consolonia.Gallery/**.*;**Example/**.*" working-directory: ./src - name: Commit files diff --git a/.github/workflows/general_build.yml b/.github/workflows/general_build.yml index d51c6edc..4bc85551 100644 --- a/.github/workflows/general_build.yml +++ b/.github/workflows/general_build.yml @@ -12,6 +12,7 @@ jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 defaults: run: working-directory: ./src diff --git a/Directory.Build.props b/Directory.Build.props index f1116aae..31a7bf2b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,12 +5,12 @@ disable - 0.10.21 + 11.0.9 https://github.com/jinek/Consolonia/graphs/contributors Text User Interface implementation of Avalonia UI (GUI Framework) Copyright © Evgeny Gorbovoy 2021 - 2022 - 0.10.21 + 11.0.9 \ No newline at end of file diff --git a/Directory.Core.Build.props b/Directory.Core.Build.props index e52f705d..90acdb61 100644 --- a/Directory.Core.Build.props +++ b/Directory.Core.Build.props @@ -10,4 +10,8 @@ true true + + true + true + \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index b052bd0f..45b7f78a 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -3224,7 +3224,7 @@ resharper_use_collection_count_property_highlighting = suggestion resharper_use_configure_await_false_for_async_disposable_highlighting = none resharper_use_configure_await_false_highlighting = suggestion resharper_use_deconstruction_highlighting = hint -resharper_use_deconstruction_on_parameter_highlighting = hint +resharper_use_deconstruction_on_parameter_highlighting = none resharper_use_empty_types_field_highlighting = suggestion resharper_use_event_args_empty_field_highlighting = suggestion resharper_use_format_specifier_in_format_string_highlighting = suggestion diff --git a/src/Consolonia.Core/ApplicationStartup.cs b/src/Consolonia.Core/ApplicationStartup.cs index 41fcb8a4..4570055f 100644 --- a/src/Consolonia.Core/ApplicationStartup.cs +++ b/src/Consolonia.Core/ApplicationStartup.cs @@ -25,29 +25,24 @@ public static class ApplicationStartup lifetime.Start(args); } - public static TAppBuilder UseStandardConsole(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + public static AppBuilder UseStandardConsole(this AppBuilder builder) { return builder.UseConsole(new DefaultNetConsole()); } - public static TAppBuilder UseConsole(this TAppBuilder builder, IConsole console) - where TAppBuilder : AppBuilderBase, new() + public static AppBuilder UseConsole(this AppBuilder builder, IConsole console) { return builder.With(console); } - public static TAppBuilder UseConsolonia(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + public static AppBuilder UseConsolonia(this AppBuilder builder) { return builder + .UseStandardRuntimePlatformSubsystem() .UseWindowingSubsystem(() => new ConsoloniaPlatform().Initialize(), nameof(ConsoloniaPlatform)) .UseRenderingSubsystem(() => { - var platformRenderInterface = - AvaloniaLocator.CurrentMutable.GetService(); - - var consoloniaRenderInterface = new ConsoloniaRenderInterface(platformRenderInterface); + var consoloniaRenderInterface = new ConsoloniaRenderInterface(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(consoloniaRenderInterface); @@ -65,8 +60,7 @@ public static ClassicDesktopStyleApplicationLifetime BuildLifetime(IConsol return CreateLifetime(consoloniaAppBuilder, args); } - private static ClassicDesktopStyleApplicationLifetime CreateLifetime(T builder, string[] args) - where T : AppBuilderBase, new() + private static ClassicDesktopStyleApplicationLifetime CreateLifetime(AppBuilder builder, string[] args) { var lifetime = new ConsoloniaLifetime { @@ -77,9 +71,8 @@ private static ClassicDesktopStyleApplicationLifetime CreateLifetime(T builde return lifetime; } - public static int StartWithConsoleLifetime( - this T builder, string[] args) - where T : AppBuilderBase, new() + public static int StartWithConsoleLifetime( + this AppBuilder builder, string[] args) { ClassicDesktopStyleApplicationLifetime lifetime = CreateLifetime(builder, args); return lifetime.Start(args); diff --git a/src/Consolonia.Core/Consolonia.Core.csproj b/src/Consolonia.Core/Consolonia.Core.csproj index cc185961..695f2d90 100644 --- a/src/Consolonia.Core/Consolonia.Core.csproj +++ b/src/Consolonia.Core/Consolonia.Core.csproj @@ -9,3 +9,4 @@ + \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs b/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs new file mode 100644 index 00000000..3ecd180f --- /dev/null +++ b/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Consolonia.Core.Drawing +{ + internal sealed class ConsoloniaPlatformRenderInterfaceContext : IPlatformRenderInterfaceContext + { + public object TryGetFeature(Type featureType) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + return new RenderTarget(surfaces); + } + + public bool IsLost => false; + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs b/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs index bf578a5d..7f03c62e 100644 --- a/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs +++ b/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs @@ -3,34 +3,15 @@ using System.IO; using Avalonia; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; -using Avalonia.Visuals.Media.Imaging; -using FormattedText = Consolonia.Core.Text.FormattedText; +using Consolonia.Core.Text; namespace Consolonia.Core.Drawing { internal class ConsoloniaRenderInterface : IPlatformRenderInterface { - private readonly IPlatformRenderInterface _platformRenderInterface; - - public ConsoloniaRenderInterface(IPlatformRenderInterface platformRenderInterface) - { - _platformRenderInterface = platformRenderInterface; - } - - public IFormattedTextImpl CreateFormattedText( - string text, - Typeface typeface, - double fontSize, - TextAlignment textAlignment, - TextWrapping wrapping, - Size constraint, - IReadOnlyList spans) - { - return new FormattedText(text, textAlignment, wrapping, constraint, spans); - //return _platformRenderInterface.CreateFormattedText(text, typeface, fontSize, textAlignment, wrapping, constraint, spans); - } - public IGeometryImpl CreateEllipseGeometry(Rect rect) { throw new NotImplementedException(); @@ -39,66 +20,61 @@ public IGeometryImpl CreateEllipseGeometry(Rect rect) public IGeometryImpl CreateLineGeometry(Point p1, Point p2) { return Line.CreateMyLine(p1, p2); - //return _platformRenderInterface.CreateLineGeometry(p1, p2); } public IGeometryImpl CreateRectangleGeometry(Rect rect) { return new Rectangle(rect); - //return _platformRenderInterface.CreateRectangleGeometry(rect); } public IStreamGeometryImpl CreateStreamGeometry() { throw new NotImplementedException(); - //return _platformRenderInterface.CreateStreamGeometry(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - return new RenderTarget(surfaces); - - //return _platformRenderInterface.CreateRenderTarget(surfaces); + throw new NotImplementedException(); } public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { - return _platformRenderInterface.CreateRenderTargetBitmap(size, dpi); + throw new NotImplementedException(); } public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) { - return _platformRenderInterface.CreateWriteableBitmap(size, dpi, format, alphaFormat); + throw new NotImplementedException(); } public IBitmapImpl LoadBitmap(string fileName) { - return _platformRenderInterface.LoadBitmap(fileName); + throw new NotImplementedException(); } public IBitmapImpl LoadBitmap(Stream stream) { - return _platformRenderInterface.LoadBitmap(stream); + throw new NotImplementedException(); } - public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + IWriteableBitmapImpl IPlatformRenderInterface.LoadWriteableBitmapToHeight(Stream stream, int height, + BitmapInterpolationMode interpolationMode) { throw new NotImplementedException(); } - public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, + public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) { throw new NotImplementedException(); @@ -114,24 +90,22 @@ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) throw new NotImplementedException(); } - public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + IBitmapImpl IPlatformRenderInterface.LoadBitmapToWidth(Stream stream, int width, + BitmapInterpolationMode interpolationMode) { - return _platformRenderInterface.LoadBitmapToWidth(stream, width, interpolationMode); + throw new NotImplementedException(); } - public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + IBitmapImpl IPlatformRenderInterface.LoadBitmapToHeight(Stream stream, int height, + BitmapInterpolationMode interpolationMode) { - return _platformRenderInterface.LoadBitmapToHeight(stream, height, interpolationMode); + throw new NotImplementedException(); } - public IBitmapImpl ResizeBitmap( - IBitmapImpl bitmapImpl, - PixelSize destinationSize, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + IBitmapImpl IPlatformRenderInterface.ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, + BitmapInterpolationMode interpolationMode) { - return _platformRenderInterface.ResizeBitmap(bitmapImpl, destinationSize, interpolationMode); + throw new NotImplementedException(); } public IBitmapImpl LoadBitmap( @@ -142,12 +116,26 @@ public IBitmapImpl LoadBitmap( Vector dpi, int stride) { - return _platformRenderInterface.LoadBitmap(format, alphaFormat, data, size, dpi, stride); + throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + IReadOnlyList glyphInfos, + Point baselineOrigin) { - return _platformRenderInterface.CreateGlyphRun(glyphRun, out width); + return new GlyphRunImpl(glyphTypeface, glyphInfos, baselineOrigin); + } + + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsApiContext) + { + if (graphicsApiContext != null) + throw new NotImplementedException("Investigate this cases"); + return new ConsoloniaPlatformRenderInterfaceContext(); + } + + public bool IsSupportedBitmapPixelFormat(PixelFormat format) + { + throw new NotImplementedException(); } public bool SupportsIndividualRoundRects => false; diff --git a/src/Consolonia.Core/Drawing/DrawingContextHelper.cs b/src/Consolonia.Core/Drawing/DrawingContextHelper.cs index e4256058..bea784af 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextHelper.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextHelper.cs @@ -9,17 +9,16 @@ internal static class DrawingContextHelper // ReSharper disable once UnusedMethodReturnValue.Global Can be used later public static bool ExecuteWithClipping(this Rect rect, Point place, Action action) { - if (!ContainsAligned(rect, place)) return false; + if (!rect.ContainsExclusive(place)) return false; action(); return true; } - public static bool ContainsAligned(this Rect rect, Point p) + public static bool IsEmpty(this Rect rect) { - (double x, double y) = p; - return x >= rect.X && x < rect.X + rect.Width && - y >= rect.Y && y < rect.Y + rect.Height; + //todo: check if this implementation serves our needs. Avalonia uses == 0 + return rect.Width <= 0 || rect.Height <= 0; } public static bool IsTranslateOnly(this Matrix transform) @@ -32,7 +31,7 @@ public static bool IsTranslateOnly(this Matrix transform) public static bool NoRotation(this Matrix transform) { - return transform.M12 == 0 && transform.M21 == 0; + return transform is { M12: 0, M21: 0 }; } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs index 3a540d18..ba90be07 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs @@ -4,14 +4,10 @@ using Avalonia; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; using Consolonia.Core.Drawing.PixelBufferImplementation; using Consolonia.Core.Infrastructure; using Consolonia.Core.InternalHelpers; -using FormattedText = Consolonia.Core.Text.FormattedText; +using Consolonia.Core.Text; namespace Consolonia.Core.Drawing { @@ -20,15 +16,12 @@ internal class DrawingContextImpl : IDrawingContextImpl private readonly Stack _clipStack = new(100); private readonly ConsoleWindow _consoleWindow; private readonly PixelBuffer _pixelBuffer; - private readonly IVisualBrushRenderer _visualBrushRenderer; - private Matrix _postTransform = Matrix.Identity; - private Matrix _transform; + private readonly Matrix _postTransform = Matrix.Identity; + private Matrix _transform = Matrix.Identity; - public DrawingContextImpl(ConsoleWindow consoleWindow, IVisualBrushRenderer visualBrushRenderer, - PixelBuffer pixelBuffer) + public DrawingContextImpl(ConsoleWindow consoleWindow, PixelBuffer pixelBuffer) { _consoleWindow = consoleWindow; - _visualBrushRenderer = visualBrushRenderer; _pixelBuffer = pixelBuffer; _clipStack.Push(pixelBuffer.Size); } @@ -51,36 +44,32 @@ public void Clear(Color color) new Pixel(new PixelBackground()));*/ } - public void DrawBitmap( - IRef source, - double opacity, - Rect sourceRect, - Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { + throw new NotImplementedException(); /* - // prototype - Rect clip = _currentClip.Intersect(destRect); - for (int x = 0; x < sourceRect.Width; x++) - for (int y = 0; y < sourceRect.Height; y++) - { - var myRenderTarget = (RenderTarget)source.Item; - - int left = (int)(x + destRect.Left); - int top = (int)(y + destRect.Top); - clip.ExecuteWithClipping(new Point(left, top), () => - { - _pixelBuffer.Set(new PixelBufferCoordinate((ushort)left, (ushort)top), destPixel => - { - return destPixel.Blend( - myRenderTarget._bufferBuffer[new PixelBufferCoordinate((ushort)(x + sourceRect.Left), - (ushort)(y + sourceRect.Top))]); - }); - }); - }*/ + // prototype + Rect clip = _currentClip.Intersect(destRect); + for (int x = 0; x < sourceRect.Width; x++) + for (int y = 0; y < sourceRect.Height; y++) + { + var myRenderTarget = (RenderTarget)source.Item; + + int left = (int)(x + destRect.Left); + int top = (int)(y + destRect.Top); + clip.ExecuteWithClipping(new Point(left, top), () => + { + _pixelBuffer.Set(new PixelBufferCoordinate((ushort)left, (ushort)top), destPixel => + { + return destPixel.Blend( + myRenderTarget._bufferBuffer[new PixelBufferCoordinate((ushort)(x + sourceRect.Left), + (ushort)(y + sourceRect.Top))]); + }); + }); + }*/ } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { throw new NotImplementedException(); } @@ -118,24 +107,21 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) if (boxShadows.Count > 0) throw new NotImplementedException(); - if (rect.Rect.IsEmpty) return; + if (rect.Rect.IsEmpty()) return; Rect r = rect.Rect; if (brush is not null) { - if (brush is IVisualBrush visualBrush) + switch (brush) { - try - { - _postTransform = Transform; - _visualBrushRenderer.RenderVisualBrush(this, visualBrush); - } - finally + case VisualBrush: + throw new NotImplementedException(); + case ISceneBrush sceneBrush: { - _postTransform = Matrix.Identity; + ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent(); + sceneBrushContent!.Render(this, Matrix.Identity); + return; } - - return; } if (brush is not FourBitColorBrush backgroundBrush) @@ -177,22 +163,14 @@ public void DrawEllipse(IBrush brush, IPen pen, Rect rect) throw new NotImplementedException(); } - public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun) { - var formattedText = (FormattedText)text; - - for (int row = 0; row < formattedText.SkiaLines.Count; row++) + if (glyphRun is not GlyphRunImpl glyphRunImpl) { - FormattedText.AvaloniaFormattedTextLine line = formattedText.SkiaLines[row]; - float x = formattedText.TransformX((float)origin.X, line.Width); - string subString = text.Text.Substring(line.Start, line.Length); - DrawStringInternal(foreground, subString, new Point(x, origin.Y + row), line.Start, - formattedText.ForegroundBrushes.Any() ? formattedText.ForegroundBrushes : null); + ConsoloniaPlatform.RaiseNotSupported(17, glyphRun); + throw new InvalidProgramException(); } - } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) - { if (glyphRun.FontRenderingEmSize.IsNearlyEqual(0)) return; if (!glyphRun.FontRenderingEmSize.IsNearlyEqual(1)) { @@ -201,7 +179,7 @@ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) } string charactersDoDraw = - string.Concat(glyphRun.GlyphIndices.Select(us => (char)us).ToArray()); + string.Concat(glyphRunImpl.GlyphIndices.Select(us => (char)us).ToArray()); DrawStringInternal(foreground, charactersDoDraw); } @@ -218,7 +196,10 @@ public void PushClip(Rect clip) public void PushClip(RoundedRect clip) { - ConsoloniaPlatform.RaiseNotSupported(2); + if (clip.IsRounded) + ConsoloniaPlatform.RaiseNotSupported(2); + + PushClip(clip.Rect); } public void PopClip() @@ -226,7 +207,7 @@ public void PopClip() _clipStack.Pop(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect? bounds) { if (opacity.IsNearlyEqual(1)) return; ConsoloniaPlatform.RaiseNotSupported(7); @@ -234,6 +215,7 @@ public void PushOpacity(double opacity) public void PopOpacity() { + throw new NotImplementedException(); } public void PushOpacityMask(IBrush mask, Rect bounds) @@ -257,21 +239,23 @@ public void PopGeometryClip() PopClip(); } - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + public void PushRenderOptions(RenderOptions renderOptions) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + public void PopRenderOptions() { throw new NotImplementedException(); } - public void Custom(ICustomDrawOperation custom) + public object GetFeature(Type t) { throw new NotImplementedException(); } + public RenderOptions RenderOptions { get; set; } + public Matrix Transform { get => _transform; @@ -282,13 +266,13 @@ public Matrix Transform { lineStyle = null; if (pen is not - { - Brush: FourBitColorBrush or LineBrush { Brush: FourBitColorBrush }, - Thickness: 1, - DashStyle: null or { Dashes: { Count: 0 } }, - LineCap: PenLineCap.Flat, - LineJoin: PenLineJoin.Miter - }) + { + Brush: FourBitColorBrush or LineBrush { Brush: FourBitColorBrush }, + Thickness: 1, + DashStyle: null or { Dashes.Count: 0 }, + LineCap: PenLineCap.Flat, + LineJoin: PenLineJoin.Miter + }) { ConsoloniaPlatform.RaiseNotSupported(6); return null; @@ -348,6 +332,7 @@ private void DrawLineInternal(IPen pen, Line line) pattern = (byte)(line.Vertical ? 0b1000 : 0b0001); DrawPixelAndMoveHead(1); //ending + return; void DrawPixelAndMoveHead(int count) { @@ -369,8 +354,7 @@ void DrawPixelAndMoveHead(int count) } } - private void DrawStringInternal(IBrush foreground, string str, Point origin = new(), int startIndex = 0, - List> additionalBrushes = null) + private void DrawStringInternal(IBrush foreground, string str, Point origin = new()) { if (foreground is not FourBitColorBrush { Mode: PixelBackgroundMode.Colored } consoleColorBrush) { @@ -384,36 +368,12 @@ void DrawPixelAndMoveHead(int count) int currentXPosition = 0; //todo: support surrogates - for (int i = 0; i < str.Length; i++) + foreach (char c in str) { Point characterPoint = whereToDraw.Transform(Matrix.CreateTranslation(currentXPosition++, 0)); ConsoleColor foregroundColor = consoleColorBrush.Color; - if (additionalBrushes != null) - { - (FormattedText.FBrushRange _, IBrush brush) = additionalBrushes.FirstOrDefault(pair => - { - // ReSharper disable once AccessToModifiedClosure //todo: pass as a parameter - int globalIndex = i + startIndex; - (FormattedText.FBrushRange key, _) = pair; - return key.StartIndex <= globalIndex && globalIndex < key.EndIndex; - }); - - if (brush != null) - { - if (brush is not FourBitColorBrush { Mode: PixelBackgroundMode.Colored } additionalBrush) - { - ConsoloniaPlatform.RaiseNotSupported(11); - return; - } - - foregroundColor = additionalBrush.Color; - } - } - - char character = str[i]; - - switch (character) + switch (c) { case '\t': { @@ -435,8 +395,8 @@ void DrawPixelAndMoveHead(int count) case '\n': { /* it's not clear if we need to draw anything. Cursor can be placed at the end of the line - var consolePixel = new Pixel(' ', foregroundColor); - + var consolePixel = new Pixel(' ', foregroundColor); + _pixelBuffer.Set((PixelBufferCoordinate)characterPoint, (oldPixel, cp) => oldPixel.Blend(cp), consolePixel);*/ } @@ -446,7 +406,7 @@ void DrawPixelAndMoveHead(int count) break; default: { - var consolePixel = new Pixel(character, foregroundColor); + var consolePixel = new Pixel(c, foregroundColor); CurrentClip.ExecuteWithClipping(characterPoint, () => { _pixelBuffer.Set((PixelBufferCoordinate)characterPoint, diff --git a/src/Consolonia.Core/Drawing/FourBitColorBrush.cs b/src/Consolonia.Core/Drawing/FourBitColorBrush.cs index 68a48810..bd523b82 100644 --- a/src/Consolonia.Core/Drawing/FourBitColorBrush.cs +++ b/src/Consolonia.Core/Drawing/FourBitColorBrush.cs @@ -1,11 +1,14 @@ using System; +using System.ComponentModel; +using System.Globalization; using Avalonia; using Avalonia.Media; using Consolonia.Core.Drawing.PixelBufferImplementation; namespace Consolonia.Core.Drawing { - public class FourBitColorBrush : Brush + [TypeConverter(typeof(FourBitBrushConverter))] + public class FourBitColorBrush : AvaloniaObject, IImmutableBrush { public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register(nameof(Color)); @@ -13,12 +16,6 @@ public class FourBitColorBrush : Brush public static readonly StyledProperty ModeProperty = AvaloniaProperty.Register(nameof(Mode)); - static FourBitColorBrush() - { - AffectsRender(ColorProperty); - AffectsRender(ModeProperty); - } - // ReSharper disable once UnusedMember.Global public FourBitColorBrush(ConsoleColor consoleColor, PixelBackgroundMode mode) : this(consoleColor) { @@ -46,18 +43,34 @@ public ConsoleColor Color set => SetValue(ColorProperty, value); } + public double Opacity => 1; + public ITransform Transform => null; + public RelativePoint TransformOrigin => RelativePoint.TopLeft; + // ReSharper disable once UnusedMember.Global used by Avalonia // ReSharper disable once UnusedParameter.Global - public Brush ProvideValue(IServiceProvider _) + public IBrush ProvideValue(IServiceProvider _) { return this; } + } + public class FourBitBrushConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } - public override IBrush ToImmutable() + public override object ConvertFrom( + ITypeDescriptorContext context, CultureInfo culture, object value) { - //todo: implement immutable - return new FourBitColorBrush { Color = Color, Mode = Mode }; + if (value is string s) + return Enum.TryParse(s, out ConsoleColor result) + ? result + : null; + + return null; } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/Line.cs b/src/Consolonia.Core/Drawing/Line.cs index 4bef09cc..7090dc2d 100644 --- a/src/Consolonia.Core/Drawing/Line.cs +++ b/src/Consolonia.Core/Drawing/Line.cs @@ -14,7 +14,7 @@ public Line(Point pStart, bool vertical, int length, IGeometryImpl sourceGeometr PStart = pStart; Vertical = vertical; Length = length; - SourceGeometry = sourceGeometry; + SourceGeometry = sourceGeometry!; // todo: check why sometimes is goes nullable? Transform = transform; } @@ -95,14 +95,15 @@ public bool TryGetSegment(double startDistance, double stopDistance, bool startO public static Line CreateMyLine(Point p1, Point p2) { (double x, double y) = p2 - p1; + // ReSharper disable once InvertIf if (p1.X > p2.X || p1.Y > p2.Y) { p1 = p2; (x, y) = (-x, -y); //todo: move this to constructor } - - return x is 0 or -0 ? new Line(p1, true, (int)y) : new Line(p1, false, (int)x); + // ReSharper disable once PatternIsRedundant todo: fix + return x is 0 or 0d ? new Line(p1, true, (int)y) : new Line(p1, false, (int)x); } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/LineBrush.cs b/src/Consolonia.Core/Drawing/LineBrush.cs index 68d5f365..7d89126c 100644 --- a/src/Consolonia.Core/Drawing/LineBrush.cs +++ b/src/Consolonia.Core/Drawing/LineBrush.cs @@ -1,23 +1,24 @@ using Avalonia; +using Avalonia.Animation; using Avalonia.Media; using Consolonia.Core.InternalHelpers; namespace Consolonia.Core.Drawing { - public class LineBrush : Brush + public class LineBrush : Animatable, IImmutableBrush { - public static readonly StyledProperty BrushProperty = - AvaloniaProperty.Register(CommonInternalHelper.GetStyledPropertyName()); + //todo: we don't really implement immutable brush + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(CommonInternalHelper.GetStyledPropertyName()); public static readonly StyledProperty LineStyleProperty = AvaloniaProperty.Register(CommonInternalHelper.GetStyledPropertyName()); static LineBrush() { - AffectsRender(BrushProperty, LineStyleProperty); } - public Brush Brush + public IBrush Brush { get => GetValue(BrushProperty); set => SetValue(BrushProperty, value); @@ -29,13 +30,9 @@ public LineStyle LineStyle set => SetValue(LineStyleProperty, value); } - public override IBrush ToImmutable() - { - return new LineBrush - { - Brush = Brush, - LineStyle = LineStyle - }; - } + //todo: how did it work without following 3 items? How should it work now, check avalonia. Search for B75ABC91-2CDD-4557-9201-16AC483C8D7B + public double Opacity => 1; + public ITransform Transform => null; + public RelativePoint TransformOrigin => RelativePoint.TopLeft; } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs b/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs index a0ab1471..0f6f89e7 100644 --- a/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs +++ b/src/Consolonia.Core/Drawing/MoveConsoleCaretToPositionBrush.cs @@ -1,9 +1,13 @@ +using Avalonia; using Avalonia.Media; namespace Consolonia.Core.Drawing { - public class MoveConsoleCaretToPositionBrush : IBrush + public class MoveConsoleCaretToPositionBrush : IImmutableBrush { + //todo: Search for B75ABC91-2CDD-4557-9201-16AC483C8D7B public double Opacity => 1; + public ITransform Transform => null; + public RelativePoint TransformOrigin => RelativePoint.TopLeft; } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/DrawingBoxSymbol.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/DrawingBoxSymbol.cs index cca0100c..b2c968e3 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/DrawingBoxSymbol.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/DrawingBoxSymbol.cs @@ -50,69 +50,44 @@ char ISymbol.GetCharacter() default: { - switch (_upRightDownLeft) + return _upRightDownLeft switch { - case EmptySymbol: return char.MinValue; - case BoldSymbol: return '█'; - - case 0b0000_1001: return '┘'; - case 0b1000_1001: return '╜'; - case 0b0001_1001: return '╛'; - case 0b1001_1001: return '╝'; - - case 0b0000_0011: return '┐'; - case 0b0010_0011: return '╖'; - case 0b0001_0011: return '╕'; - case 0b0011_0011: return '╗'; - - case 0b0000_0110: return '┌'; - case 0b0100_0110: return '╒'; - case 0b0010_0110: return '╓'; - case 0b0110_0110: return '╔'; - - case 0b0000_1100: return '└'; - case 0b0100_1100: return '╘'; - case 0b1000_1100: return '╙'; - case 0b1100_1100: return '╚'; - - case 0b0000_1110: return '├'; - case 0b1000_1110: - case 0b0010_1110: - case 0b1010_1110: return '╟'; - case 0b0100_1110: return '╞'; - case 0b1100_1110: - case 0b0110_1110: - case 0b1110_1110: return '╠'; - - case 0b0000_1011: return '┤'; - case 0b1000_1011: - case 0b0010_1011: - case 0b1010_1011: return '╢'; - case 0b0001_1011: return '╡'; - case 0b1001_1011: - case 0b0011_1011: - case 0b1011_1011: return '╣'; - - case 0b0000_1101: return '┴'; - case 0b1000_1101: return '╨'; - case 0b0100_1101: - case 0b0001_1101: - case 0b0101_1101: return '╧'; - case 0b1100_1101: - case 0b1001_1101: - case 0b1101_1101: return '╩'; - - case 0b0000_0111: return '┬'; - case 0b0010_0111: return '╥'; - case 0b0100_0111: - case 0b0001_0111: - case 0b0101_0111: return '╤'; - case 0b0110_0111: - case 0b0011_0111: - case 0b0111_0111: return '╦'; - - default: throw new InvalidOperationException(); - } + EmptySymbol => char.MinValue, + BoldSymbol => '█', + 0b0000_1001 => '┘', + 0b1000_1001 => '╜', + 0b0001_1001 => '╛', + 0b1001_1001 => '╝', + 0b0000_0011 => '┐', + 0b0010_0011 => '╖', + 0b0001_0011 => '╕', + 0b0011_0011 => '╗', + 0b0000_0110 => '┌', + 0b0100_0110 => '╒', + 0b0010_0110 => '╓', + 0b0110_0110 => '╔', + 0b0000_1100 => '└', + 0b0100_1100 => '╘', + 0b1000_1100 => '╙', + 0b1100_1100 => '╚', + 0b0000_1110 => '├', + 0b1000_1110 or 0b0010_1110 or 0b1010_1110 => '╟', + 0b0100_1110 => '╞', + 0b1100_1110 or 0b0110_1110 or 0b1110_1110 => '╠', + 0b0000_1011 => '┤', + 0b1000_1011 or 0b0010_1011 or 0b1010_1011 => '╢', + 0b0001_1011 => '╡', + 0b1001_1011 or 0b0011_1011 or 0b1011_1011 => '╣', + 0b0000_1101 => '┴', + 0b1000_1101 => '╨', + 0b0100_1101 or 0b0001_1101 or 0b0101_1101 => '╧', + 0b1100_1101 or 0b1001_1101 or 0b1101_1101 => '╩', + 0b0000_0111 => '┬', + 0b0010_0111 => '╥', + 0b0100_0111 or 0b0001_0111 or 0b0101_0111 => '╤', + 0b0110_0111 or 0b0011_0111 or 0b0111_0111 => '╦', + _ => throw new InvalidOperationException() + }; } } } diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs index 3bddf91d..99748959 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs @@ -57,7 +57,6 @@ public Pixel Blend(Pixel pixelAbove) { PixelForeground newForeground; PixelBackground newBackground; - bool newIsCaret; switch (pixelAbove.Background.Mode) { @@ -66,16 +65,16 @@ public Pixel Blend(Pixel pixelAbove) case PixelBackgroundMode.Transparent: newForeground = Foreground.Blend(pixelAbove.Foreground); newBackground = Background; - newIsCaret = IsCaret | pixelAbove.IsCaret; break; case PixelBackgroundMode.Shaded: (newForeground, newBackground) = Shade(); newForeground = newForeground.Blend(pixelAbove.Foreground); - newIsCaret = IsCaret | pixelAbove.IsCaret; break; default: throw new ArgumentOutOfRangeException(nameof(pixelAbove)); } + bool newIsCaret = IsCaret | pixelAbove.IsCaret; + return new Pixel(newForeground, newBackground, newIsCaret); } diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBufferCoordinate.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBufferCoordinate.cs index bccbde4d..24da206e 100644 --- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBufferCoordinate.cs +++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBufferCoordinate.cs @@ -48,5 +48,10 @@ public static PixelBufferCoordinate ToPixelBufferCoordinate(Point point) { return (PixelBufferCoordinate)point; } + + public bool Equals(PixelBufferCoordinate secondPoint) + { + return X == secondPoint.X && Y == secondPoint.Y; + } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/Rectangle.cs b/src/Consolonia.Core/Drawing/Rectangle.cs index dec67f9e..ad001372 100644 --- a/src/Consolonia.Core/Drawing/Rectangle.cs +++ b/src/Consolonia.Core/Drawing/Rectangle.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Media; using Avalonia.Platform; @@ -12,7 +13,7 @@ internal class Rectangle : ITransformedGeometryImpl public Rectangle(Rect rect, IGeometryImpl sourceGeometry = default, Matrix transform = default) { _rect = rect; - SourceGeometry = sourceGeometry; + SourceGeometry = sourceGeometry!; Transform = transform; } @@ -25,7 +26,7 @@ public Rect GetRenderBounds(IPen pen) public bool FillContains(Point point) { - return _rect.ContainsAligned(point); + return _rect.ContainsExclusive(point); } public IGeometryImpl Intersect(IGeometryImpl geometry) @@ -33,16 +34,16 @@ public IGeometryImpl Intersect(IGeometryImpl geometry) throw new NotImplementedException(); } - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains([NotNull] IPen pen, Point point) { - if (pen.Thickness == 0) + if (pen!.Thickness == 0) return false; // ReSharper disable once CompareOfFloatsByEqualityOperator if (pen.Thickness != 1) throw new NotImplementedException(); if (!FillContains(point)) return false; - return !_rect.Deflate(1).ContainsAligned(point); + return !_rect.Deflate(1).ContainsExclusive(point); } public ITransformedGeometryImpl WithTransform(Matrix transform) diff --git a/src/Consolonia.Core/Drawing/RenderTarget.cs b/src/Consolonia.Core/Drawing/RenderTarget.cs index c7346ecf..d4cacc4e 100644 --- a/src/Consolonia.Core/Drawing/RenderTarget.cs +++ b/src/Consolonia.Core/Drawing/RenderTarget.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Avalonia; +using Avalonia.Controls; using Avalonia.Platform; -using Avalonia.Rendering; using Consolonia.Core.Drawing.PixelBufferImplementation; using Consolonia.Core.Infrastructure; @@ -40,17 +40,12 @@ public void Dispose() _consoleWindow.Resized -= OnResized; } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new DrawingContextImpl(_consoleWindow, visualBrushRenderer, _bufferBuffer); - } - - public void Save(string fileName) + public void Save(string fileName, int? quality = null) { throw new NotImplementedException(); } - public void Save(Stream stream) + public void Save(Stream stream, int? quality = null) { throw new NotImplementedException(); } @@ -59,7 +54,7 @@ public void Save(Stream stream) public PixelSize PixelSize { get; } = new(1, 1); public int Version => 0; - public void Blit(IDrawingContextImpl context) + void IDrawingContextLayerImpl.Blit(IDrawingContextImpl context) { try { @@ -70,10 +65,19 @@ public void Blit(IDrawingContextImpl context) } } - public bool CanBlit => true; + bool IDrawingContextLayerImpl.CanBlit => true; - private void OnResized(Size size, PlatformResizeReason platformResizeReason) + public IDrawingContextImpl CreateDrawingContext() { + return new DrawingContextImpl(_consoleWindow, _bufferBuffer); + } + + public bool IsCorrupted => false; + + + private void OnResized(Size size, WindowResizeReason reason) + { + // todo: should we check the reason? InitializeBuffer(size); } @@ -114,11 +118,12 @@ private void RenderToDevice() caretPosition = new PixelBufferCoordinate(x, y); } - if (!_consoleWindow.InvalidatedRects.Any(rect => - rect.ContainsAligned(new Point(x, y)))) continue; + /* todo: There is not IWindowImpl.Invalidate anymore. + if (!_consoleWindow.InvalidatedRects.Any(rect => + rect.ContainsExclusive(new Point(x, y)))) continue;*/ if (pixel.Background.Mode != PixelBackgroundMode.Colored) throw new InvalidOperationException( - "Buffer has not been rendered. All operations over buffer must finished with the buffer to be not transparent"); + "All pixels in the buffer must have exact console color before rendering"); if (x == pixelBuffer.Width - 1 && y == pixelBuffer.Height - 1) break; @@ -144,7 +149,7 @@ private void RenderToDevice() } if (char.IsControl(character) /*|| character is '保' or '哥'*/) - character = ' '; // some terminals does not print \0 + character = ' '; // some terminals do not print \0 ConsoleColor backgroundColor = pixel.Background.Color; ConsoleColor foregroundColor = pixel.Foreground.Color; @@ -172,8 +177,6 @@ private void RenderToDevice() { _console.CaretVisible = false; } - - _consoleWindow.InvalidatedRects.Clear(); } private struct FlushingBuffer diff --git a/src/Consolonia.Core/Helpers/Extensions.cs b/src/Consolonia.Core/Helpers/Extensions.cs new file mode 100644 index 00000000..5c2f4f27 --- /dev/null +++ b/src/Consolonia.Core/Helpers/Extensions.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia; +using Avalonia.Reactive; + +namespace Consolonia.Core.Helpers +{ + public static class Extensions + { + public static void SubscribeAction( + this IObservable> observable, + Action> action) + { + observable.Subscribe(new AnonymousObserver>(action)); + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/AdvancedDeferredRenderer.cs b/src/Consolonia.Core/Infrastructure/AdvancedDeferredRenderer.cs deleted file mode 100644 index 72ce4231..00000000 --- a/src/Consolonia.Core/Infrastructure/AdvancedDeferredRenderer.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Avalonia; -using Avalonia.Rendering; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace Consolonia.Core.Infrastructure -{ - internal class AdvancedDeferredRenderer : DeferredRenderer, IRenderer - { - private readonly IVisual _root; - public ConsoleWindow RenderRoot; - - public AdvancedDeferredRenderer(IRenderRoot root, IRenderLoop renderLoop, ISceneBuilder sceneBuilder = null, - IDispatcher dispatcher = null, IDeferredRendererLock rendererLock = null) : base(root, renderLoop, - sceneBuilder, dispatcher, rendererLock) - { - _root = root; - } - - void IRenderer.AddDirty(IVisual visual) - { - base.AddDirty(visual); - - // this is from immediateRenderer, keeping original formatting - // todo: consider how to call the original method (get rid of copypaste here) - // ReSharper disable InvertIf - // ReSharper disable SuggestVarOrType_SimpleTypes - if (visual.Bounds != Rect.Empty) - { - var m = visual.TransformToVisual(_root); - - if (m.HasValue) - { - var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value); - - //use transformedbounds as previous render state of the visual bounds - //so we can invalidate old and new bounds of a control in case it moved/shrinked - if (visual.TransformedBounds.HasValue) - { - var trb = visual.TransformedBounds.Value; - var trBounds = trb.Bounds.TransformToAABB(trb.Transform); - - if (trBounds != bounds) RenderRoot?.Invalidate(trBounds); - } - - RenderRoot?.Invalidate(bounds); - } - } - // ReSharper restore SuggestVarOrType_SimpleTypes - // ReSharper restore InvertIf - } - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs b/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs index ed9bdf3e..a2562213 100644 --- a/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs +++ b/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs @@ -1,16 +1,34 @@ using System; using System.Linq; using Avalonia; +using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Rendering; using Avalonia.VisualTree; using Consolonia.Core.InternalHelpers; namespace Consolonia.Core.Infrastructure { - public class ArrowsAndKeyboardNavigationHandler : KeyboardNavigationHandler, IKeyboardNavigationHandler + public class ArrowsAndKeyboardNavigationHandler : IKeyboardNavigationHandler { - public new void Move(IInputElement element, NavigationDirection direction, + private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; + + //todo: check XTFocus https://github.com/jinek/Consolonia/issues/105#issuecomment-2089015880 + private IInputRoot _owner; + + public ArrowsAndKeyboardNavigationHandler(IKeyboardNavigationHandler keyboardNavigationHandler) + { + _keyboardNavigationHandler = keyboardNavigationHandler; + } + + public void SetOwner(IInputRoot owner) + { + _keyboardNavigationHandler.SetOwner(owner); + //todo: should we RemoveHandler here? + _owner = owner; + _owner.AddHandler(InputElement.KeyDownEvent, new EventHandler(OnKeyDown)); + } + + public void Move(IInputElement element, NavigationDirection direction, KeyModifiers keyModifiers = KeyModifiers.None) { if (direction is NavigationDirection.Right or @@ -18,11 +36,12 @@ NavigationDirection.Left or NavigationDirection.Down or NavigationDirection.Up) { - IRenderRoot visualRoot = element.GetVisualRoot(); - (Point p1, Point p2) = GetOriginalPoint(element.TransformedBounds.NotNull().Clip); + var elementCast = (InputElement)element; + var visualRoot = (Visual)elementCast.GetVisualRoot(); + (Point p1, Point p2) = GetOriginalPoint(elementCast.GetTransformedBounds().NotNull().Clip); Point originalPoint = p1 / 2 + p2 / 2; - var focusableElements = visualRoot.GetVisualDescendants() + var focusableElements = visualRoot!.GetVisualDescendants() .OfType() // only focusable .Where(inputElement => inputElement.Focusable && inputElement.IsEffectivelyEnabled && @@ -33,7 +52,7 @@ NavigationDirection.Down or .Select(inputElement => { (Point firstTargetPoint, Point secondTargetPoint) = - GetTargetPoint(inputElement.TransformedBounds.NotNull().Clip); + GetTargetPoint(inputElement.GetTransformedBounds().NotNull().Clip); return new { vector = @@ -49,20 +68,22 @@ NavigationDirection.Down or Point vector = arg.vector; return IsInCone(vector); }) - // selecting closest one + // selecting the closest one .MinBy(arg => { - Point coordinates = arg.vector; - return coordinates.X * coordinates.X + coordinates.Y * coordinates.Y; + (double x, double y) = arg.vector; + return x * x + y * y; }); - focusableElements?.inputElement.Focus(); + focusableElements?.inputElement?.Focus(); } else { - base.Move(element, direction, keyModifiers); + _keyboardNavigationHandler.Move(element, direction, keyModifiers); } + return; + (Point, Point) GetOriginalPoint(Rect valueClip) { #pragma warning disable CS8509 @@ -99,12 +120,12 @@ bool IsInCone(Point vector) } } - protected override void OnKeyDown(object sender, KeyEventArgs e) + private void OnKeyDown(object sender, KeyEventArgs e) { - base.OnKeyDown(sender, e); - if (e.Handled) return; - IInputElement current = FocusManager.Instance?.Current; + + //see FocusManager.GetFocusManager + IInputElement current = TopLevel.GetTopLevel((Visual)sender)!.FocusManager!.GetFocusedElement(); if (e.KeyModifiers != KeyModifiers.None) return; diff --git a/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs b/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs index d3c49aed..f7f03703 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Platform; -using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Consolonia.Core.Drawing.PixelBufferImplementation; -using JetBrains.Annotations; namespace Consolonia.Core.Infrastructure { @@ -17,7 +18,6 @@ internal class ConsoleWindow : IWindowImpl { private readonly IKeyboardDevice _myKeyboardDevice; [NotNull] internal readonly IConsole Console; - internal readonly List InvalidatedRects = new(50); private IInputRoot _inputRoot; public ConsoleWindow() @@ -29,8 +29,11 @@ public ConsoleWindow() Console.KeyEvent += ConsoleOnKeyEvent; Console.MouseEvent += ConsoleOnMouseEvent; Console.FocusEvent += ConsoleOnFocusEvent; + Handle = null!; } + private IMouseDevice MouseDevice { get; } + public void Dispose() { Closed?.Invoke(); @@ -41,45 +44,6 @@ public void Dispose() Console.Dispose(); } - public IRenderer CreateRenderer(IRenderRoot root) - { - /*return new X11ImmediateRendererProxy(root, AvaloniaLocator.Current.GetService()) - { DrawDirtyRects = false, DrawFps = false };*/ - return new AdvancedDeferredRenderer(root, AvaloniaLocator.Current.GetService()) - { - RenderRoot = this - // RenderOnlyOnRenderThread = true - }; - } - - public void Invalidate(Rect rect) - { - if (rect.IsEmpty) return; - InvalidatedRects.Add(rect); - - - /* - This is the code for drawing invalid rectangles - var _console = AvaloniaLocator.Current.GetService(); - using (_console.StoreCaret()) - { - for (int y = (int)rect.Y; y < rect.Bottom; y++) - { - if (y < Console.WindowHeight - 2) - { - Console.SetCursorPosition((int)rect.X, y); - Console.BackgroundColor = ConsoleColor.Magenta; - Console.ForegroundColor = ConsoleColor.Magenta; - - Console.WriteLine(string.Concat(Enumerable.Range(0, (int)rect.Width).Select(i => ' '))); - } - } - }*/ - //Paint(new Rect(0, 0, ClientSize.Width, ClientSize.Height)); - - //Paint(new Rect(rect.Left, rect.Top, rect.Width, rect.Height)); - } - public void SetInputRoot(IInputRoot inputRoot) { _inputRoot = inputRoot; @@ -106,8 +70,14 @@ public IPopupImpl CreatePopup() return null; // when returning null top window overlay layer will be used } - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + public void SetTransparencyLevelHint(IReadOnlyList transparencyLevels) + { + throw new NotImplementedException("Consider this"); + } + + public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { + //todo: } public Size ClientSize @@ -127,15 +97,16 @@ public Size ClientSize public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } + public Compositor Compositor { get; } = new(null); public Action Closed { get; set; } public Action LostFocus { get; set; } - public IMouseDevice MouseDevice { get; } public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; @@ -144,7 +115,7 @@ public Size ClientSize public void Show(bool activate, bool isDialog) { if (activate) - Activated(); + Activated!(); } public void Hide() @@ -162,7 +133,7 @@ public void SetTopmost(bool value) throw new NotImplementedException(); } - public double DesktopScaling { get; } = 1d; + public double DesktopScaling => 1d; // ReSharper disable once UnassignedGetOnlyAutoProperty todo: get from _console if supported public PixelPoint Position { get; } @@ -177,7 +148,7 @@ public void SetTopmost(bool value) public Size MaxAutoSizeHint { get; } // ReSharper disable once UnassignedGetOnlyAutoProperty todo: what is this property - public IScreenImpl Screen { get; } + public IScreenImpl Screen => null!; public void SetTitle(string title) { @@ -222,11 +193,12 @@ public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) throw new NotImplementedException(); } - public void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application) + public void Resize(Size clientSize, WindowResizeReason reason = WindowResizeReason.Application) { - Resized(clientSize, reason); + //todo: can we deny resizing? } + public void Move(PixelPoint point) { throw new NotImplementedException(); @@ -255,7 +227,7 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } public Action GotInputWhenDisabled { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } // ReSharper disable once UnassignedGetOnlyAutoProperty todo: what is this property public bool IsClientAreaExtendedToDecorations { get; } @@ -270,12 +242,21 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) // ReSharper disable once UnassignedGetOnlyAutoProperty todo: what is this property public Thickness OffScreenMargin { get; } + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(ISystemNavigationManagerImpl)) + return null; + if (featureType == typeof(ITextInputMethodImpl)) return null; + throw new NotImplementedException("Consider this"); + } + private void ConsoleOnMouseEvent(RawPointerEventType type, Point point, Vector? wheelDelta, RawInputModifiers modifiers) { ulong timestamp = (ulong)Stopwatch.GetTimestamp(); Dispatcher.UIThread.Post(() => { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (type) { case RawPointerEventType.Move: @@ -290,12 +271,12 @@ private void ConsoleOnMouseEvent(RawPointerEventType type, Point point, Vector? case RawPointerEventType.MiddleButtonUp: case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton2Up: - Input(new RawPointerEventArgs(MouseDevice, timestamp, _inputRoot, + Input!(new RawPointerEventArgs(MouseDevice, timestamp, _inputRoot, type, point, modifiers)); break; case RawPointerEventType.Wheel: - Input(new RawMouseWheelEventArgs(MouseDevice, timestamp, _inputRoot, point, + Input!(new RawMouseWheelEventArgs(MouseDevice, timestamp, _inputRoot, point, (Vector)wheelDelta!, modifiers)); break; } @@ -318,8 +299,7 @@ private void OnConsoleOnResized() { PixelBufferSize pixelBufferSize = Console.Size; var size = new Size(pixelBufferSize.Width, pixelBufferSize.Height); - Resized(size, PlatformResizeReason.Unspecified); - //todo; Invalidate(new Rect(size)); + Resized!(size, WindowResizeReason.Unspecified); }); } @@ -330,9 +310,12 @@ private async void ConsoleOnKeyEvent(Key key, char keyChar, RawInputModifiers ra { Dispatcher.UIThread.Post(() => { - Input(new RawKeyEventArgs(_myKeyboardDevice, timeStamp, _inputRoot, +#pragma warning disable CS0618 // Type or member is obsolete // todo: change to correct constructor, CFA20A9A-3A24-4187-9CA3-9DF0081124EE + var rawInputEventArgs = new RawKeyEventArgs(_myKeyboardDevice, timeStamp, _inputRoot, RawKeyEventType.KeyUp, key, - rawInputModifiers)); + rawInputModifiers); +#pragma warning restore CS0618 // Type or member is obsolete + Input!(rawInputEventArgs); }, DispatcherPriority.Input); } else @@ -340,18 +323,20 @@ private async void ConsoleOnKeyEvent(Key key, char keyChar, RawInputModifiers ra bool handled = false; await Dispatcher.UIThread.InvokeAsync(() => { +#pragma warning disable CS0618 // Type or member is obsolete //todo: CFA20A9A-3A24-4187-9CA3-9DF0081124EE var rawInputEventArgs = new RawKeyEventArgs(_myKeyboardDevice, timeStamp, _inputRoot, RawKeyEventType.KeyDown, key, rawInputModifiers); - Input(rawInputEventArgs); +#pragma warning restore CS0618 // Type or member is obsolete + Input!(rawInputEventArgs); handled = rawInputEventArgs.Handled; - }, DispatcherPriority.Input).ConfigureAwait(true); + }, DispatcherPriority.Input).GetTask().ConfigureAwait(true); if (!handled && !char.IsControl(keyChar)) Dispatcher.UIThread.Post(() => { - Input(new RawTextInputEventArgs(_myKeyboardDevice, + Input!(new RawTextInputEventArgs(_myKeyboardDevice, timeStamp, _inputRoot, keyChar.ToString())); diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs index fc2ebd46..198513e0 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs @@ -7,9 +7,10 @@ public class ConsoloniaApplication : Application { public override void RegisterServices() { - var keyboardNavigationHandler = AvaloniaLocator.Current.GetService(); base.RegisterServices(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(keyboardNavigationHandler); + var keyboardNavigationHandler = AvaloniaLocator.Current.GetRequiredService(); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new ArrowsAndKeyboardNavigationHandler(keyboardNavigationHandler)); } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaException.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaException.cs index 700ef495..2693c330 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaException.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaException.cs @@ -1,14 +1,14 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using JetBrains.Annotations; namespace Consolonia.Core.Infrastructure - // ReSharper disable once ArrangeNamespaceBody { - [UsedImplicitly] public class ConsoloniaException : ApplicationException { + // ReSharper disable UnusedMember.Global public ConsoloniaException() + { } @@ -16,13 +16,14 @@ protected ConsoloniaException([NotNull] SerializationInfo info, StreamingContext { } - public ConsoloniaException([CanBeNull] string message) : base(message) + public ConsoloniaException(string message) : base(message) { } - public ConsoloniaException([CanBeNull] string message, [CanBeNull] Exception innerException) : base(message, + public ConsoloniaException(string message, Exception innerException) : base(message, innerException) { } + // ReSharper restore UnusedMember.Global } } \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaLifetime.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaLifetime.cs index a4b800c0..ffbf9270 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaLifetime.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaLifetime.cs @@ -15,8 +15,8 @@ public Task DisconnectFromConsoleAsync(CancellationToken cancellationToken) var taskToWaitFor = new TaskCompletionSource(); cancellationToken.Register(() => taskToWaitFor.SetResult()); - var mainWindowPlatformImpl = (ConsoleWindow)MainWindow.PlatformImpl; - IConsole console = mainWindowPlatformImpl.Console; + var mainWindowPlatformImpl = (ConsoleWindow)MainWindow!.PlatformImpl; + IConsole console = mainWindowPlatformImpl!.Console; Task pauseTask = taskToWaitFor.Task; @@ -29,7 +29,7 @@ public Task DisconnectFromConsoleAsync(CancellationToken cancellationToken) Dispatcher.UIThread.Post(() => { MainWindow.InvalidateVisual(); }); }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - return Dispatcher.UIThread.InvokeAsync(() => { }); + return Dispatcher.UIThread.InvokeAsync(() => { }).GetTask(); } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs index 07c61b38..f65c7f2c 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs @@ -2,12 +2,12 @@ using System.Diagnostics; using Avalonia; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Threading; using Consolonia.Core.Dummy; using Consolonia.Core.Text; @@ -23,7 +23,7 @@ public IWindowImpl CreateWindow() public IWindowImpl CreateEmbeddableWindow() { RaiseNotSupported(13); - return null; + return null!; } public ITrayIconImpl CreateTrayIcon() @@ -37,21 +37,25 @@ public void Initialize() AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) - .Bind().ToSingleton() + /*todo: need replacement? .Bind().ToSingleton()*/ .Bind().ToConstant(new UiThreadRenderTimer(120)) - .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(new ManagedDispatcherImpl(null)) + /*.Bind().ToConstant(new SleepLoopRenderTimer(120))*/ + /*SleepLoopRenderTimer : IRenderTimer*/ + /*.Bind().ToConstant(new RenderLoop()) todo: is internal now*/ .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new ConsoleKeyboardDevice()) .Bind().ToConstant(new FontManagerImpl()) - .Bind().ToConstant(new TextShaperImpl()) + .Bind().ToConstant(new TextShaper()) .Bind().ToConstant(new MouseDevice()) .Bind().ToConstant(new DummyCursorFactory()) .Bind().ToConstant(new DummyIconLoader()) - .Bind().ToTransient() + .Bind().ToSingleton() + /*.Bind().ToTransient() todo: implement this navigation*/ //.Bind().ToConstant(new X11Clipboard(this)) //.Bind().ToConstant(new PlatformSettingsStub()) //.Bind().ToConstant(new GtkSystemDialog()) - .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); + /*.Bind().ToConstant(new LinuxMountedVolumeInfoProvider())*/; } [DebuggerStepThrough] @@ -77,4 +81,9 @@ notSupportedRequest.Information[1] is null && } } } + + public class ConsoloniaPlatformSettings : DefaultPlatformSettings + { + //todo: + } } \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatformThreadingInterface.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaPlatformThreadingInterface.cs deleted file mode 100644 index 3c145b4f..00000000 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatformThreadingInterface.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Avalonia.Controls.Platform; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Consolonia.Core.Infrastructure -{ - /// - /// Implements special - /// - internal class ConsoloniaPlatformThreadingInterface : InternalPlatformThreadingInterface, - IPlatformThreadingInterface - { - public new IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - return tick.Target is DispatcherTimer { Tag: ICaptureTimerStartStop captureTimerStartStop } - ? new ConsoloniaTextPresenterPointerBlinkFakeTimer(captureTimerStartStop) - : base.StartTimer(priority, interval, tick); - } - - private class ConsoloniaTextPresenterPointerBlinkFakeTimer : IDisposable - { - private readonly ICaptureTimerStartStop _captureTimerStartStop; - - public ConsoloniaTextPresenterPointerBlinkFakeTimer(ICaptureTimerStartStop captureTimerStartStop) - { - _captureTimerStartStop = captureTimerStartStop; - _captureTimerStartStop.CaptureTimerStart(); - } - - - public void Dispose() - { - _captureTimerStartStop.CaptureTimerStop(); - } - } - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ExceptionSink.cs b/src/Consolonia.Core/Infrastructure/ExceptionSink.cs index 76335f7e..717fdf66 100644 --- a/src/Consolonia.Core/Infrastructure/ExceptionSink.cs +++ b/src/Consolonia.Core/Infrastructure/ExceptionSink.cs @@ -1,6 +1,6 @@ using System; using System.Globalization; -using Avalonia.Controls; +using Avalonia; using Avalonia.Logging; namespace Consolonia.Core.Infrastructure @@ -16,8 +16,7 @@ public bool IsEnabled(LogEventLevel level, string area) public void Log(LogEventLevel level, string area, object source, string messageTemplate) { Log(level, area, source, messageTemplate, Array.Empty()); - } - + } // ReSharper disable UnusedMember.Global public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) { Log(level, area, source, messageTemplate, new object[] { propertyValue0 }); @@ -59,10 +58,11 @@ public void Log(LogEventLevel level, string area, object source, string messageT public static class ExceptionSinkExtensions { - public static T LogToException(this T builder) where T : AppBuilderBase, new() + public static AppBuilder LogToException(this AppBuilder builder) { Logger.Sink = new ExceptionSink(); return builder; } } + // ReSharper restore UnusedMember.Global } \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/ICaptureTimerStartStop.cs b/src/Consolonia.Core/Infrastructure/ICaptureTimerStartStop.cs deleted file mode 100644 index 6d025e7d..00000000 --- a/src/Consolonia.Core/Infrastructure/ICaptureTimerStartStop.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Consolonia.Core.Infrastructure -{ - public interface ICaptureTimerStartStop - { - void CaptureTimerStart(); - void CaptureTimerStop(); - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs index 7f114fc3..02f59ca8 100644 --- a/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs +++ b/src/Consolonia.Core/Infrastructure/InputLessDefaultNetConsole.cs @@ -78,20 +78,12 @@ public void Print(PixelBufferCoordinate bufferPoint, ConsoleColor backgroundColo _headForeground = Console.ForegroundColor = foregroundColor; if (!str.IsNormalized(NormalizationForm.FormKC)) - str = str.Normalize(NormalizationForm.FormKC); + throw new NotSupportedException("Is not supposed to be rendered"); if (str.Any( - c => ConsoleText.IsWideChar(c) && - char.IsLetterOrDigit(c) /*todo: https://github.com/SlimeNull/NullLib.ConsoleEx/issues/2*/)) - { - StringBuilder stringBuilder = new(); - foreach (char c in str) - stringBuilder.Append(ConsoleText.IsWideChar(c) && char.IsLetterOrDigit(c) - ? '?' //todo: support wide characters - : c); - - str = stringBuilder.ToString(); - } + c => ConsoleText.IsWideChar(c) && + char.IsLetterOrDigit(c) /*todo: https://github.com/SlimeNull/NullLib.ConsoleEx/issues/2*/)) + throw new NotSupportedException("Is not supposed to be rendered"); Console.Write(str); diff --git a/src/Consolonia.Core/Infrastructure/InvalidDrawingContextException.cs b/src/Consolonia.Core/Infrastructure/InvalidDrawingContextException.cs index cca84cfb..5cb22638 100644 --- a/src/Consolonia.Core/Infrastructure/InvalidDrawingContextException.cs +++ b/src/Consolonia.Core/Infrastructure/InvalidDrawingContextException.cs @@ -1,21 +1,20 @@ using System; -using JetBrains.Annotations; - -// ReSharper disable UnusedMember.Global +using System.Diagnostics.CodeAnalysis; namespace Consolonia.Core.Infrastructure { + [SuppressMessage("ReSharper", "UnusedMember.Global")] public class InvalidDrawingContextException : ApplicationException { public InvalidDrawingContextException() { } - public InvalidDrawingContextException([CanBeNull] string message) : base(message) + public InvalidDrawingContextException(string message) : base(message) { } - public InvalidDrawingContextException([CanBeNull] string message, [CanBeNull] Exception innerException) : base( + public InvalidDrawingContextException(string message, Exception innerException) : base( message, innerException) { } diff --git a/src/Consolonia.Core/Infrastructure/NotSupportedRequest.cs b/src/Consolonia.Core/Infrastructure/NotSupportedRequest.cs index a4f3501e..303ae466 100644 --- a/src/Consolonia.Core/Infrastructure/NotSupportedRequest.cs +++ b/src/Consolonia.Core/Infrastructure/NotSupportedRequest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -7,7 +8,7 @@ namespace Consolonia.Core.Infrastructure { public sealed class NotSupportedRequest { - public NotSupportedRequest(int errorCode, object[] information) + public NotSupportedRequest(int errorCode, IList information) { ErrorCode = errorCode; Information = new ReadOnlyCollection(information); diff --git a/src/Consolonia.Core/InternalHelpers/CommonInternalHelper.cs b/src/Consolonia.Core/InternalHelpers/CommonInternalHelper.cs index e3671ad0..dc15318f 100644 --- a/src/Consolonia.Core/InternalHelpers/CommonInternalHelper.cs +++ b/src/Consolonia.Core/InternalHelpers/CommonInternalHelper.cs @@ -1,5 +1,5 @@ -using System; using System.Runtime.CompilerServices; +using Consolonia.Core.Infrastructure; namespace Consolonia.Core.InternalHelpers { @@ -7,11 +7,7 @@ internal static class CommonInternalHelper { public static bool IsNearlyEqual(this double value, double compareTo) { - return value.CompareTo(compareTo) == 0; - } - - public static bool IsNearlyEqual(this float value, float compareTo) - { + //todo: strange implementation for this name return value.CompareTo(compareTo) == 0; } @@ -23,7 +19,7 @@ public static string GetStyledPropertyName([CallerMemberName] string propertyFul public static T NotNull(this T? t) where T : struct { if (t == null) - throw new InvalidOperationException("Value is not expected to be null"); + throw new ConsoloniaException("Value is not expected to be null"); return t.Value; } } diff --git a/src/Consolonia.Core/Styles/ResourceIncludeBase.cs b/src/Consolonia.Core/Styles/ResourceIncludeBase.cs index a63a3be0..0b41173d 100644 --- a/src/Consolonia.Core/Styles/ResourceIncludeBase.cs +++ b/src/Consolonia.Core/Styles/ResourceIncludeBase.cs @@ -34,6 +34,7 @@ public IStyle Loaded { get { + // ReSharper disable once InvertIf if (_loaded == null) { _isLoading = true; @@ -46,9 +47,10 @@ public IStyle Loaded } } - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, ThemeVariant theme, out object value) { - if (!_isLoading && Loaded is IResourceProvider p) return p.TryGetResource(key, out value); + if (!_isLoading && Loaded is IResourceProvider p) + return p.TryGetResource(key, theme, out value); value = null; return false; @@ -80,10 +82,11 @@ public event EventHandler OwnerChanged } } - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) + /*todo: remove + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) { return Loaded.TryAttach(target, host); - } + }*/ public IReadOnlyList Children => _loaded ?? Array.Empty(); } diff --git a/src/Consolonia.Core/Text/FontManagerImpl.cs b/src/Consolonia.Core/Text/FontManagerImpl.cs index 78043556..c3c53874 100644 --- a/src/Consolonia.Core/Text/FontManagerImpl.cs +++ b/src/Consolonia.Core/Text/FontManagerImpl.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; @@ -13,30 +13,37 @@ internal class FontManagerImpl : IFontManagerImpl { public string GetDefaultFontFamilyName() { - return "ConsoleDefault(F7D6533C-AC9D-4C4A-884F-7719A9B5DC0C)"; + return GetTheOnlyFontFamilyName(); } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) { - throw new NotImplementedException(); + return new[] { GetTheOnlyFontFamilyName() }; } - public bool TryMatchCharacter( - int codepoint, - FontStyle fontStyle, - FontWeight fontWeight, - FontFamily fontFamily, - CultureInfo culture, - out Typeface typeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo culture, out Typeface typeface) { throw new NotImplementedException(); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, + out IGlyphTypeface glyphTypeface) { - //todo: check defaults or rause platform error + //todo: check font is ours the only + glyphTypeface = new GlyphTypeface(); + return true; + } - return new TypefaceImpl(); + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + throw new NotImplementedException(); + } + + public static string GetTheOnlyFontFamilyName() + { + return "ConsoleDefault(F7D6533C-AC9D-4C4A-884F-7719A9B5DC0C)"; } } } \ No newline at end of file diff --git a/src/Consolonia.Core/Text/FormattedText.cs b/src/Consolonia.Core/Text/FormattedText.cs deleted file mode 100644 index a681b555..00000000 --- a/src/Consolonia.Core/Text/FormattedText.cs +++ /dev/null @@ -1,469 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia; -using Avalonia.Media; -using Avalonia.Platform; -using Consolonia.Core.InternalHelpers; - -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedMember.Global - -namespace Consolonia.Core.Text -{ - // todo: Copy-paste of avalonia skia. Probably must be simplified or re-used from skia - internal class FormattedText : IFormattedTextImpl - { - private const float MaxLineWidth = 10000; //todo: copied from avalonia, magic number - private readonly List _lines = new(); - private readonly List _rects = new(); - private readonly TextAlignment _textAlignment; - private readonly TextWrapping _wrapping; - - public FormattedText(string text, TextAlignment textAlignment, - TextWrapping wrapping, Size constraint, IReadOnlyList spans) - { - Text = text ?? string.Empty; - // Replace 0 characters with zero-width spaces (200B) - Text = Text.Replace((char)0, (char)0x200B); - - _textAlignment = textAlignment; - _wrapping = wrapping; - Constraint = constraint; - - if (spans != null) - foreach (FormattedTextStyleSpan span in spans) - if (span.ForegroundBrush != null) - SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length); - - Rebuild(); - } - - internal List> ForegroundBrushes { get; } = new(); - internal List SkiaLines { get; private set; } - - - public IEnumerable GetLines() - { - return _lines; - } - - public TextHitTestResult HitTestPoint(Point point) - { - float y = (float)point.Y; - - AvaloniaFormattedTextLine line = default; - - float nextTop = 0; - - foreach (AvaloniaFormattedTextLine currentLine in SkiaLines) - if (currentLine.Top <= y) - { - line = currentLine; - nextTop = currentLine.Top + 1; - } - else - { - nextTop = currentLine.Top; - break; - } - - if (!line.Equals(default(AvaloniaFormattedTextLine))) - { - var rects = GetRects(); - - for (int c = line.Start; c < line.Start + line.Length; c++) - { - Rect rc = rects[c]; - if (rc.Contains(point)) - return new TextHitTestResult - { - IsInside = true, - TextPosition = c, - IsTrailing = point.X - rc.X > rc.Width / 2 - }; - } - - int offset = 0; - - if (point.X >= rects[line.Start].X + line.Width && line.Length > 0) - offset = line.Length - 1; - - if (y < nextTop) - return new TextHitTestResult - { - IsInside = false, - TextPosition = line.Start + offset, - IsTrailing = Text.Length == line.Start + offset + 1 - }; - } - - bool end = point.X > Bounds.Width || point.Y > _lines.Count; - - return new TextHitTestResult - { - IsInside = false, - IsTrailing = end, - TextPosition = end ? Text.Length - 1 : 0 - }; - } - - public Rect HitTestTextPosition(int index) - { - const int caretWidth = 1; - if (string.IsNullOrEmpty(Text)) - { - float alignmentOffset = TransformX(0, 0); - return new Rect(alignmentOffset, 0, caretWidth, 1); - } - - var rects = GetRects(); - - if (index < Text.Length && index >= 0) return rects[index]; - Rect r = rects.LastOrDefault(); - - char c = Text[^1]; - - switch (c) - { - case '\n': - case '\r': - return new Rect(r.X, r.Y, caretWidth, 1); - default: - return new Rect(r.X + r.Width, r.Y, caretWidth, 1); - } - } - - public IEnumerable HitTestTextRange(int index, int length) - { - var result = new List(); - - var rects = GetRects(); - - int lastIndex = index + length - 1; - - foreach (AvaloniaFormattedTextLine line in SkiaLines.Where(l => - l.Start + l.Length > index && - lastIndex >= l.Start)) - { - int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0); - - double left = rects[line.Start > index ? line.Start : index].X; - double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right; - - result.Add(new Rect(left, line.Top, right - left, 1)); - } - - return result; - } - - - public Size Constraint { get; } - public Rect Bounds { get; private set; } - public string Text { get; } - - private void SetForegroundBrush(IBrush brush, int startIndex, int length) - { - var key = new FBrushRange(startIndex, length); - int index = ForegroundBrushes.FindIndex(v => v.Key.Equals(key)); - - if (index > -1) ForegroundBrushes.RemoveAt(index); - - if (brush != null) - { - brush = brush.ToImmutable(); - ForegroundBrushes.Insert(0, new KeyValuePair(key, brush)); - } - } - - - private void Rebuild() - { - int length = Text.Length; - - _lines.Clear(); - _rects.Clear(); - SkiaLines = new List(); - - int curOff = 0; - float curY = 0; - - float widthConstraint = double.IsPositiveInfinity(Constraint.Width) - ? -1 - : (float)Constraint.Width; - - while (curOff < length) - { - float constraint = -1; - - if (_wrapping == TextWrapping.Wrap) - { - constraint = widthConstraint <= 0 ? MaxLineWidth : widthConstraint; - if (constraint > MaxLineWidth) - constraint = MaxLineWidth; - } - - int measured = LineBreak(Text, curOff, length, constraint, out int trailingnumber); - var line = new AvaloniaFormattedTextLine - { - Start = curOff, - Length = measured - }; - string subString = Text.Substring(line.Start, line.Length); - float lineWidth = subString.Length; - line.Length = measured - trailingnumber; - line.Width = lineWidth; - line.Top = curY; - - SkiaLines.Add(line); - - curY += 1; - curOff += measured; - - //if this is the last line and there are trailing newline characters then - //insert a additional line - if (curOff < length) continue; - string subStringMinusNewlines = subString.TrimEnd('\n', '\r'); - int lengthDiff = subString.Length - subStringMinusNewlines.Length; - if (lengthDiff <= 0) continue; - string lastLineSubString = Text.Substring(line.Start, line.Length); - int lastLineWidth = lastLineSubString.Length; - var lastLine = new AvaloniaFormattedTextLine - { - Length = lengthDiff, - Start = curOff - lengthDiff, - Top = curY, - Width = lastLineWidth - }; - - SkiaLines.Add(lastLine); - - curY += 1; - } - - // Now convert to Avalonia data formats - _lines.Clear(); - float maxX = 0; - - for (int c = 0; c < SkiaLines.Count; c++) - { - float w = SkiaLines[c].Width; - if (maxX < w) - maxX = w; - - _lines.Add(new FormattedTextLine(SkiaLines[c].Length, 1)); - } - - if (SkiaLines.Count == 0) - { - _lines.Add(new FormattedTextLine(0, 1)); - Bounds = new Rect(0, 0, 0, 1); - } - else - { - AvaloniaFormattedTextLine lastLine = SkiaLines[^1]; - Bounds = new Rect(0, 0, maxX, lastLine.Top + 1); - - if (double.IsPositiveInfinity(Constraint.Width)) return; - - Bounds = _textAlignment switch - { - TextAlignment.Center => new Rect(Constraint).CenterRect(Bounds), - TextAlignment.Right => new Rect(Constraint.Width - Bounds.Width, 0, Bounds.Width, Bounds.Height), - _ => Bounds - }; - } - } - - private static int LineBreak(string textInput, int textIndex, int stop, - float maxWidth, - out int trailingCount) - { - int lengthBreak; - if (maxWidth.IsNearlyEqual(-1)) - lengthBreak = stop - textIndex; - else - lengthBreak = (int)maxWidth; - - //Check for white space or line breakers before the lengthBreak - int index = textIndex; - int wordStart = textIndex; - bool prevBreak = true; - - trailingCount = 0; - - while (index < stop) - { - int prevText = index; - char currChar = textInput[index++]; - bool currBreak = IsBreakChar(currChar); - - if (!currBreak && prevBreak) wordStart = prevText; - - prevBreak = currBreak; - - if (index > textIndex + lengthBreak) - { - if (currBreak) - { - // eat the rest of the whitespace - while (index < stop && IsBreakChar(textInput[index])) index++; - - trailingCount = index - prevText; - } - else - { - // backup until a whitespace (or 1 char) - if (wordStart == textIndex) - { - if (prevText > textIndex) index = prevText; - } - else - { - index = wordStart; - } - } - - break; - } - - if ('\n' == currChar) - { - int ret = index - textIndex; - int lineBreakSize = 1; - if (index < stop) - { - currChar = textInput[index++]; - if ('\r' == currChar) - { - ret = index - textIndex; - ++lineBreakSize; - } - } - - trailingCount = lineBreakSize; - - return ret; - } - - if ('\r' == currChar) - { - int ret = index - textIndex; - int lineBreakSize = 1; - if (index < stop) - { - currChar = textInput[index++]; - if ('\n' == currChar) - { - ret = index - textIndex; - ++lineBreakSize; - } - } - - trailingCount = lineBreakSize; - - return ret; - } - } - - return index - textIndex; - } - - private static bool IsBreakChar(char c) - { - //white space or zero space whitespace - return char.IsWhiteSpace(c) || c == '\u200B'; - } - - internal float TransformX(float originX, float lineWidth) - { - float x = 0; - - if (_textAlignment == TextAlignment.Left) - { - x = originX; - } - else - { - double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) - ? Constraint.Width - : Bounds.Width; - - x = _textAlignment switch - { - TextAlignment.Center => originX + (float)(width - lineWidth) / 2, - TextAlignment.Right => originX + (float)(width - lineWidth), - _ => x - }; - } - - return x; - } - - private List GetRects() - { - if (Text.Length > _rects.Count) BuildRects(); - - return _rects; - } - - private void BuildRects() - { - // Build character rects - - for (int li = 0; li < SkiaLines.Count; li++) - { - AvaloniaFormattedTextLine line = SkiaLines[li]; - float prevRight = TransformX(0, line.Width); - double nextTop = line.Top + 1; - - if (li + 1 < SkiaLines.Count) nextTop = SkiaLines[li + 1].Top; - - for (int i = line.Start; i < line.Start + line.Length; i++) - { - float w = 1; - - _rects.Add(new Rect( - prevRight, - line.Top, - w, - nextTop - line.Top)); - prevRight += w; - } - } - } - - internal struct AvaloniaFormattedTextLine - { - public int Length; - public int Start; - public float Top; - public float Width; - } - - internal readonly struct FBrushRange - { - public FBrushRange(int startIndex, int length) - { - StartIndex = startIndex; - Length = length; - } - - public int EndIndex => StartIndex + Length; - - public int Length { get; } - - public int StartIndex { get; } - - public bool Intersects(int index, int len) - { - return index + len > StartIndex && - StartIndex + Length > index; - } - - public override string ToString() - { - return $"{StartIndex}-{EndIndex}"; - } - } - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Text/GlyphRunImpl.cs b/src/Consolonia.Core/Text/GlyphRunImpl.cs new file mode 100644 index 00000000..7c141569 --- /dev/null +++ b/src/Consolonia.Core/Text/GlyphRunImpl.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; + +namespace Consolonia.Core.Text +{ + internal class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(IGlyphTypeface glyphTypeface, IReadOnlyList glyphInfos, Point baselineOrigin) + { + FontRenderingEmSize = glyphTypeface.Metrics.DesignEmHeight; + GlyphTypeface = glyphTypeface; + BaselineOrigin = baselineOrigin; + GlyphIndices = glyphInfos.Select(info => info.GlyphIndex).ToArray(); + Bounds = new Rect(new Point(0, 0), + new Size(glyphInfos.Sum(info => info.GlyphAdvance), FontRenderingEmSize)); + } + + public ushort[] GlyphIndices { get; } + + public void Dispose() + { + } + + public IReadOnlyList GetIntersections(float lowerLimit, float upperLimit) + { + throw new NotImplementedException(); + } + + public IGlyphTypeface GlyphTypeface { get; } + + public double FontRenderingEmSize { get; } + public Point BaselineOrigin { get; } + public Rect Bounds { get; } + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Text/GlyphTypeface.cs b/src/Consolonia.Core/Text/GlyphTypeface.cs new file mode 100644 index 00000000..c8d28083 --- /dev/null +++ b/src/Consolonia.Core/Text/GlyphTypeface.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using Avalonia.Media; + +namespace Consolonia.Core.Text +{ + public sealed class GlyphTypeface : IGlyphTypeface + { + public void Dispose() + { + } + + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) + { + /*todo: handle special characters here?*/ + metrics = new GlyphMetrics + { + XBearing = 0, + YBearing = 0, + Height = 1, + Width = 1 + }; + return true; + } + + public ushort GetGlyph(uint codepoint) + { + checked + { + return (ushort)codepoint; + } + } + + public bool TryGetGlyph(uint codepoint, out ushort glyph) + { + glyph = (ushort)codepoint; + return true; + } + + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + checked + { + return codepoints.ToArray().Select(u => (ushort)u).ToArray(); + } + } + + public int GetGlyphAdvance(ushort glyph) + { + return 1; + } + + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + return Enumerable.Repeat(1, glyphs.Length).ToArray(); + } + + public bool TryGetTable(uint tag, out byte[] table) + { + throw new NotImplementedException(); + } + + public string FamilyName { get; } = FontManagerImpl.GetTheOnlyFontFamilyName(); + public FontWeight Weight => FontWeight.Normal; + public FontStyle Style => FontStyle.Normal; + public FontStretch Stretch => FontStretch.Normal; + public int GlyphCount => char.MaxValue; + + public FontMetrics Metrics { get; } = new() + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01#funits-and-the-em-square + DesignEmHeight = 1, + Ascent = -1, //var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; //todo: need to consult Avalonia guys + Descent = 0, + LineGap = 0, + UnderlinePosition = 0, + UnderlineThickness = 0, + StrikethroughPosition = 0, + StrikethroughThickness = 0, + IsFixedPitch = true + }; + + public FontSimulations FontSimulations => FontSimulations.None; + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Text/TextShaper.cs b/src/Consolonia.Core/Text/TextShaper.cs new file mode 100644 index 00000000..84a26c9d --- /dev/null +++ b/src/Consolonia.Core/Text/TextShaper.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Text; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using NullLib.ConsoleEx; + +namespace Consolonia.Core.Text +{ + public class TextShaper : ITextShaperImpl + { + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) + { + var glyphInfos = Convert(text.Span.ToString()); + + var shapedBuffer = new ShapedBuffer(text, glyphInfos.Length, + new GlyphTypeface(), 1, 0 /*todo: must be 1 for right to left?*/); + + for (int i = 0; i < shapedBuffer.Length; i++) shapedBuffer[i] = glyphInfos[i]; + return shapedBuffer; + } + + public static GlyphInfo[] Convert(string str) + { + if (!str.IsNormalized(NormalizationForm.FormKC)) + str = str.Normalize(NormalizationForm.FormKC); + + // ReSharper disable once InvertIf + if (str.Any( + c => ConsoleText.IsWideChar(c) && + char.IsLetterOrDigit(c) /*todo: https://github.com/SlimeNull/NullLib.ConsoleEx/issues/2*/)) + { + StringBuilder stringBuilder = new(); + foreach (char c in str) + stringBuilder.Append(ConsoleText.IsWideChar(c) && char.IsLetterOrDigit(c) + ? '?' //todo: support wide characters + : c); + + str = stringBuilder.ToString(); + } + + return str.Select((c, index) => new GlyphInfo(c, index, 1)).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Core/Text/TextShaperImpl.cs b/src/Consolonia.Core/Text/TextShaperImpl.cs deleted file mode 100644 index 3de07fc5..00000000 --- a/src/Consolonia.Core/Text/TextShaperImpl.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using Avalonia; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Consolonia.Core.Text -{ - internal class TextShaperImpl : ITextShaperImpl - { - public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, - CultureInfo culture) - { - //todo: check defaults (but can draw with font size = 0 in theory) - var glyphIndices = new ReadOnlySlice(text.Select(c => (ushort)c).ToArray()); - var glyphAdvances = - new ReadOnlySlice(new ReadOnlyMemory(text.Select(_ => 1d).ToArray())); - var glyphClusters = - new ReadOnlySlice(new ReadOnlyMemory(text.Select((_, i) => (ushort)i).ToArray())); - - return new GlyphRun( - new GlyphTypeface(typeface), - fontRenderingEmSize, - glyphIndices, - glyphAdvances, - new ReadOnlySlice(null), - text, - glyphClusters /*todo: must be 1 for right to left*/); - } - } -} \ No newline at end of file diff --git a/src/Consolonia.Core/Text/TypefaceImpl.cs b/src/Consolonia.Core/Text/TypefaceImpl.cs deleted file mode 100644 index 449fc9bd..00000000 --- a/src/Consolonia.Core/Text/TypefaceImpl.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Linq; -using Avalonia.Platform; - -namespace Consolonia.Core.Text -{ - internal class TypefaceImpl : IGlyphTypefaceImpl - { - public void Dispose() - { - } - - public ushort GetGlyph(uint codepoint) - { - checked - { - return (ushort)codepoint; - } - } - - public ushort[] GetGlyphs(ReadOnlySpan codepoints) - { - checked - { - return codepoints.ToArray().Select(u => (ushort)u).ToArray(); - } - } - - public int GetGlyphAdvance(ushort glyph) - { - return 1; - } - - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - return Enumerable.Repeat(1, glyphs.Length).ToArray(); - } - - /// - /// https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01#funits-and-the-em-square - /// - public short DesignEmHeight => 1; - - public int Ascent => - -1; //var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; - - public int Descent => 0; - public int LineGap => 0; - public int UnderlinePosition => 0; - public int UnderlineThickness => 0; - public int StrikethroughPosition => 0; - public int StrikethroughThickness => 0; - public bool IsFixedPitch => true; - } -} \ No newline at end of file diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml index b537afbb..208dac67 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryAnimatedLines.axaml @@ -1,11 +1,6 @@ diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryButton.axaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryButton.axaml.cs index 6ddc1c60..dfe84a0d 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryButton.axaml.cs +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryButton.axaml.cs @@ -1,12 +1,10 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -using JetBrains.Annotations; namespace Consolonia.Gallery.Gallery.GalleryViews { - [UsedImplicitly] [GalleryOrder(30)] - public class GalleryButton : UserControl + public partial class GalleryButton : UserControl { public GalleryButton() { diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml index a2a5266d..affbacab 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml @@ -1,10 +1,5 @@  diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml.cs index 69322aab..b4e2ca33 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml.cs +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendar.axaml.cs @@ -1,13 +1,11 @@ using System; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using JetBrains.Annotations; namespace Consolonia.Gallery.Gallery.GalleryViews { - [UsedImplicitly] [GalleryOrder(30)] - public class GalleryCalendar : UserControl + public partial class GalleryCalendar : UserControl { public GalleryCalendar() { diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendarPicker.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendarPicker.axaml index f19a44e4..4dc9f356 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendarPicker.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryCalendarPicker.axaml @@ -1,12 +1,7 @@  diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTabControl.axaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTabControl.axaml.cs index 89cbd33f..2ff611b3 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTabControl.axaml.cs +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTabControl.axaml.cs @@ -1,13 +1,11 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -using JetBrains.Annotations; // ReSharper disable UnusedAutoPropertyAccessor.Global namespace Consolonia.Gallery.Gallery.GalleryViews { - [UsedImplicitly] - public class GalleryTabControl : UserControl + public partial class GalleryTabControl : UserControl { public GalleryTabControl() { diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml index 9af15e1b..f58d8e2f 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryTextBlock.axaml @@ -1,11 +1,6 @@ - + + ItemsPanel="{TemplateBinding ItemsPanel}"> \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/DataGrid.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/DataGrid.axaml index a8200b27..876ead88 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/DataGrid.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/DataGrid.axaml @@ -15,6 +15,8 @@ Value="Stretch" /> + - + @@ -277,8 +279,6 @@ - + + + diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/DataValidationErrors.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/DataValidationErrors.axaml index 04c6c34d..c87fce10 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/DataValidationErrors.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/DataValidationErrors.axaml @@ -20,7 +20,7 @@ - diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogHelpers.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogHelpers.cs index a5a1a27c..a7407024 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogHelpers.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogHelpers.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.VisualTree; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Dialog { @@ -26,7 +27,7 @@ public class DialogHost static DialogHost() { - IsDialogHostProperty.Changed.Subscribe(args => + IsDialogHostProperty.Changed.SubscribeAction(args => { args.Sender.SetValue(DialogHostProperty, args.NewValue.Value ? new DialogHost((Window)args.Sender) : null); @@ -40,8 +41,9 @@ private DialogHost(Window window) public void OpenInternal(DialogWindow dialogWindow) { + IInputElement focusedElement = _window.FocusManager! /*todo: low: Why can be null?*/.GetFocusedElement(); var overlayLayer = OverlayLayer.GetOverlayLayer(_window); - var popupHost = new OverlayPopupHost(overlayLayer); + var popupHost = new OverlayPopupHost(overlayLayer!); popupHost.ConfigurePosition(_window, PlacementMode.AnchorAndGravity, new Point(), PopupAnchor.TopLeft, PopupGravity.BottomRight); @@ -53,18 +55,19 @@ public void OpenInternal(DialogWindow dialogWindow) if (_dialogs.TryPeek(out OverlayPopupHost previousDialog)) previousDialog.IsEnabled = false; - dialogWrap.HadFocusOn = FocusManager.Instance!.Current; + dialogWrap.HadFocusOn = focusedElement; _dialogs.Push(popupHost); popupHost.Show(); dialogWindow.AttachedToVisualTree += DialogAttachedToVisualTree; + return; static void DialogAttachedToVisualTree(object sender, EventArgs e) { var dialogWindow = (DialogWindow)sender!; dialogWindow.AttachedToVisualTree -= DialogAttachedToVisualTree; - FocusManager.Instance!.Focus(dialogWindow); + dialogWindow.Focus(); } } @@ -72,15 +75,15 @@ private ContentPresenter GetFirstContentPresenter() { ContentPresenter firstContentPresenter = _window.GetTemplateChildren() .Select(control => control.FindDescendantOfType()) - .First(d => d.Name == "PART_ContentPresenter"); + .First(d => d!.Name == "PART_ContentPresenter"); return firstContentPresenter; } public void PopInternal(DialogWindow dialogWindow) { OverlayPopupHost overlayPopupHost = _dialogs.Pop(); - var dialogWrap = (DialogWrap)overlayPopupHost.Content; - if (!Equals(dialogWrap.ContentPresenter.Content, dialogWindow)) + var dialogWrap = (DialogWrap)overlayPopupHost.Content!; + if (!Equals(dialogWrap!.FoundContentPresenter.Content, dialogWindow!)) throw new InvalidOperationException("Dialog is not topmost. Close private dialogs first"); overlayPopupHost.Hide(); @@ -97,7 +100,7 @@ public void PopInternal(DialogWindow dialogWindow) firstContentPresenter.Focus(); } - FocusManager.Instance!.Focus(dialogWrap.HadFocusOn); + dialogWrap.HadFocusOn.Focus(); } } } \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogWindow.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogWindow.cs index b6829a29..e9fb484e 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogWindow.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Dialog/DialogWindow.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Consolonia.Themes.TurboVision.Templates.Controls.Dialog { + [TemplatePart("PART_ContentPresenter", typeof(ContentPresenter))] public class DialogWindow : UserControl { public static readonly DirectProperty ContentSizeProperty = @@ -22,7 +24,7 @@ public class DialogWindow : UserControl public static readonly StyledProperty IsCloseButtonVisibleProperty = AvaloniaProperty.Register(nameof(IsCloseButtonVisible), true); - private Size _contentSize = Size.Empty; + private Size _contentSize; private ContentPresenter _partContentPresenter; private TaskCompletionSource _taskCompletionSource; @@ -66,23 +68,22 @@ public void CloseClick() CloseDialog(); } - [Obsolete("Avalonia is deprecating this")] - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); - _partContentPresenter = (ContentPresenter)e.NameScope.Find("PART_ContentPresenter"); + base.OnApplyTemplate(e); + _partContentPresenter = e.NameScope.Find("PART_ContentPresenter"); } protected override Size ArrangeOverride(Size finalSize) { Size arrangeOverride = base.ArrangeOverride(finalSize); - IVisual firstVisualChild = _partContentPresenter?.GetVisualChildren().FirstOrDefault(); + Visual firstVisualChild = _partContentPresenter?.GetVisualChildren().FirstOrDefault(); if (firstVisualChild != null) ContentSize = firstVisualChild.Bounds.Size; return arrangeOverride; } - private void ShowDialogInternal(IControl parent) + private void ShowDialogInternal(Visual parent) { DialogHost dialogHost = GetDialogHost(parent); dialogHost.OpenInternal(this); @@ -96,7 +97,7 @@ public virtual void CloseDialog() _taskCompletionSource.SetResult(); } - public Task ShowDialogAsync(IControl parent) + public Task ShowDialogAsync(Control parent) { if (_taskCompletionSource != null) throw new NotImplementedException(); @@ -106,10 +107,10 @@ public Task ShowDialogAsync(IControl parent) return _taskCompletionSource.Task; } - private static DialogHost GetDialogHost(IControl parent) + private static DialogHost GetDialogHost(Visual parent) { var window = parent.FindAncestorOfType(true); - DialogHost dialogHost = window.GetValue(DialogHost.DialogHostProperty); + DialogHost dialogHost = window!.GetValue(DialogHost.DialogHostProperty); return dialogHost; } diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/DialogWrap.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/DialogWrap.axaml index a3a4778d..1e1d7c95 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/DialogWrap.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/DialogWrap.axaml @@ -1,18 +1,15 @@ - + ("ContentPresenter"); + AttachedToVisualTree += (_, _) => { var parentWindow = this.FindAncestorOfType(); - _disposable = parentWindow.GetPropertyChangedObservable(TopLevel.ClientSizeProperty).Subscribe(args => - { - var newSize = (Size)args.NewValue!; + _disposable = parentWindow!.GetPropertyChangedObservable(TopLevel.ClientSizeProperty).Subscribe( + new AnonymousObserver( + args => + { + var newSize = (Size)args.NewValue!; - SetNewSize(newSize); - }); - SetNewSize(parentWindow.ClientSize); + SetNewSize(newSize); + })); + SetNewSize(parentWindow!.ClientSize); }; DetachedFromLogicalTree += (_, _) => { _disposable.Dispose(); }; } - internal ContentPresenter ContentPresenter => this.Get("ContentPresenter"); - /// /// Focused element when new dialog shown /// This is focus to restore when dialog closed @@ -51,14 +59,7 @@ private void InitializeComponent() public void SetContent(DialogWindow dialogWindow) { - /*_disposable2?.Dispose(); - _disposable2 = dialogWindow.GetPropertyChangedObservable(BoundsProperty).Subscribe(args => - { - var rect = (Rect)args.NewValue; - DialogPanelBorder.Width = rect.Width + 2; - DialogPanelBorder.Height = rect.Height + 2; - });*/ - ContentPresenter.Content = dialogWindow; + FoundContentPresenter.Content = dialogWindow; } // ReSharper disable once UnusedMember.Local Example of usage for further (when mouse support introduced for example) @@ -66,7 +67,7 @@ public void SetContent(DialogWindow dialogWindow) private void CloseDialog() #pragma warning restore IDE0051 { - ((DialogWindow)ContentPresenter.Content).CloseDialog(); + ((DialogWindow)FoundContentPresenter!.Content!)!.CloseDialog(); } } } \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/AreIntegersEqualConverter.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/AreIntegersEqualConverter.cs new file mode 100644 index 00000000..b900d59d --- /dev/null +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/AreIntegersEqualConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers +{ + public class AreIntegersEqualConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + int[] ints = values.Cast().ToArray(); + int first = ints.FirstOrDefault(); + + for (int i = 1; i < values.Count; i++) + if (ints[i] != first) + return false; + + return true; + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/BooleanAndConverter.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/BooleanAndConverter.cs new file mode 100644 index 00000000..44c4ea24 --- /dev/null +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/BooleanAndConverter.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers +{ + public class BooleanAndConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + bool convert = values.All(val => (bool)val); + return convert; + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ButtonExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ButtonExtensions.cs index c8252e41..69fc5d7e 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ButtonExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ButtonExtensions.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -17,7 +18,7 @@ public static class ButtonExtensions static ButtonExtensions() { - Button.ClickEvent.Raised.Subscribe(async tuple => + Button.ClickEvent.Raised.Subscribe(new AnonymousObserver<(object, RoutedEventArgs)>(async tuple => { (object sender, RoutedEventArgs e) = tuple; if (sender != e.Source) return; @@ -32,7 +33,7 @@ static ButtonExtensions() await Task.Delay(timeout).ConfigureAwait(true); //todo: magic number PseudolassesExtensions.Set(button.Classes, ":clickdelayed", false); - }); + })); } } } \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarDatePickerExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarDatePickerExtensions.cs index 21f0b6ec..688656c7 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarDatePickerExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarDatePickerExtensions.cs @@ -1,10 +1,10 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Threading; using Avalonia.VisualTree; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -27,7 +27,7 @@ static CalendarDatePickerExtensions() calendarDatePicker.KeyUp -= CalendarOnKeyUp; }); - FocusOnOpenProperty.Changed.Subscribe(args => + FocusOnOpenProperty.Changed.SubscribeAction(args => { DropDownExtensions.ProcessFocusOnOpen(args, CalendarDatePicker.IsDropDownOpenProperty, @@ -40,6 +40,7 @@ static CalendarDatePickerExtensions() await Task.Yield(); Dispatcher.UIThread.Post(() => { textBox?.Focus(); }); }); + return; static void FocusOnDropDown(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarExtensions.cs index 8ea08be4..0f079e26 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/CalendarExtensions.cs @@ -1,7 +1,7 @@ -using System; -using Avalonia; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -12,7 +12,7 @@ public static class CalendarExtensions static CalendarExtensions() { - ZoomOutOnKeyProperty.Changed.Subscribe(args => + ZoomOutOnKeyProperty.Changed.SubscribeAction(args => { var calendar = (Calendar)args.Sender; diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ComboBoxExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ComboBoxExtensions.cs index df51e2f9..91072c1a 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ComboBoxExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ComboBoxExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using Avalonia; using Avalonia.Controls; @@ -6,6 +5,7 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Threading; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -27,11 +27,12 @@ static ComboBoxExtensions() box.KeyDown -= ComboBoxOnKeyDown; }); - FocusOnOpenProperty.Changed.Subscribe(args => + FocusOnOpenProperty.Changed.SubscribeAction(args => { DropDownExtensions.ProcessFocusOnOpen(args, ComboBox.IsDropDownOpenProperty, FocusOnDropDown, comboBox => comboBox.Focus()); + return; static void FocusOnDropDown(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ConsoloniaTextPresenter.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ConsoloniaTextPresenter.cs index 76747e70..6e0c0d36 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ConsoloniaTextPresenter.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ConsoloniaTextPresenter.cs @@ -1,69 +1,97 @@ using System; using System.Reflection; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Threading; using Consolonia.Core.Drawing; -using Consolonia.Core.Infrastructure; +using Consolonia.Core.Drawing.PixelBufferImplementation; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { - public class ConsoloniaTextPresenter : TextPresenter, ICaptureTimerStartStop + public class ConsoloniaTextPresenter : TextPresenter { + // ReSharper disable once MemberCanBePrivate.Global + public static readonly StyledProperty CaretPositionProperty = + AvaloniaProperty.Register(nameof(CaretPosition)); + private static readonly FieldInfo TickTimerField = typeof(TextPresenter).GetField("_caretTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - private bool _caretBlinking; - static ConsoloniaTextPresenter() { - CaretBrushProperty.Changed.Subscribe(static args => + SelectionEndProperty.Changed.Subscribe(new AnonymousObserver>(args => { - if (args.NewValue.Value is not MoveConsoleCaretToPositionBrush) - throw new NotSupportedException(); - }); - } + if (args.Sender is not ConsoloniaTextPresenter textPresenter) + return; - public ConsoloniaTextPresenter() - { - var caretTickTimer = (DispatcherTimer)TickTimerField.GetValue(this); - caretTickTimer!.Tag = this; + textPresenter.UpdateCaretPosition(null); + })); - CaretBrush = new MoveConsoleCaretToPositionBrush(); - } + CaretIndexProperty.Changed + .SubscribeAction(args => + { + if (args.Sender is not ConsoloniaTextPresenter textPresenter) + return; - public void CaptureTimerStart() - { - _caretBlinking = true; - } + // once avalonia moved the caret we then moving it additionally to scroll outside the boundaries - public void CaptureTimerStop() - { - _caretBlinking = false; + int caretIndex = args.NewValue.Value; + + Rect hitTestTextPosition = textPresenter.UpdateCaretPosition(caretIndex); + + Dispatcher.UIThread.Post( + () => + { + textPresenter.BringIntoView(new Rect(hitTestTextPosition.X, hitTestTextPosition.Y, 1, 1)); + }, + DispatcherPriority + .UiThreadRender /*Must be lower than DispatcherPriority.AfterRender which is used by TextPresenter*/); + }); + + CaretBrushProperty.Changed + .Subscribe( + new AnonymousObserver>( + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local //todo: what does this mean? + args => + { + if (args.NewValue.Value is not FourBitColorBrush + { + Color: ConsoleColor.Black, Mode: PixelBackgroundMode.Transparent + }) + throw new NotSupportedException(); + })); } - public override void Render(DrawingContext context) + public ConsoloniaTextPresenter() { - base.Render(context); - if (SelectionStart == SelectionEnd || !_caretBlinking) return; - (Point p1, Point p2) = GetCaretPoints(); - context.DrawLine( - new Pen(CaretBrush), - p1, p2); + // we need to disable blinking caret, our terminal caret blinks itself once shown + var caretTickTimer = (DispatcherTimer)TickTimerField.GetValue(this); + caretTickTimer!.Interval = + TimeSpan.FromMilliseconds(int + .MaxValue); //see DispatcherTimer.Interval, since we can not disable it, setting it to the longest interval possible + caretTickTimer!.Tick += (_, _) => throw new NotImplementedException("How to disable timer completely?"); + + CaretBrush = + new FourBitColorBrush(ConsoleColor.Black, PixelBackgroundMode.Transparent); // we want to draw own caret } - private (Point, Point) GetCaretPoints() + public Point CaretPosition { - return ((Point, Point))typeof(TextPresenter) - .GetMethod(nameof(GetCaretPoints), BindingFlags.Instance | BindingFlags.NonPublic)! - .Invoke(this, null)!; + get => GetValue(CaretPositionProperty); + private set => SetValue(CaretPositionProperty, value); } - protected override Size MeasureOverride(Size availableSize) + private Rect UpdateCaretPosition(int? caretIndex) { - Size measureOverride = base.MeasureOverride(availableSize); - return measureOverride.WithWidth(measureOverride.Width + 1); + caretIndex ??= CaretIndex; + + Rect hitTestTextPosition = TextLayout.HitTestTextPosition(caretIndex.Value); + CaretPosition = new Point(hitTestTextPosition.X, hitTestTextPosition.Y); + return hitTestTextPosition; } } } \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/DropDownExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/DropDownExtensions.cs index c5e73792..6a8594a2 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/DropDownExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/DropDownExtensions.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.LogicalTree; +using Avalonia.Reactive; using Avalonia.Threading; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers @@ -24,24 +25,24 @@ public static void ProcessFocusOnOpen(AvaloniaProp { dropDownControl.AttachedToVisualTree += focusDropDownAction; - IDisposable disposable1 = parentControl.GetPropertyChangedObservable(InputElement.IsFocusedProperty) - .Subscribe(eventArgs => + IDisposable disposable1 = parentControl!.GetPropertyChangedObservable(InputElement.IsFocusedProperty) + .Subscribe(new AnonymousObserver(eventArgs => { if (!(bool)eventArgs.NewValue! && !dropDownControl.IsKeyboardFocusWithin) - Dispatcher.UIThread.Post(() => { parentControl.SetValue(dropDownProperty, false); }); - }); + Dispatcher.UIThread.Post(() => { parentControl!.SetValue(dropDownProperty, false); }); + })); IDisposable disposable2 = dropDownControl .GetPropertyChangedObservable(InputElement.IsKeyboardFocusWithinProperty) - .Subscribe(eventArgs => + .Subscribe(new AnonymousObserver(eventArgs => { - if (!(bool)eventArgs.NewValue! && !parentControl.IsKeyboardFocusWithin) + if (!(bool)eventArgs.NewValue! && !parentControl!.IsKeyboardFocusWithin) Dispatcher.UIThread.Post(() => { - parentControl.SetValue(dropDownProperty, false); - focusParentAction(parentControl); + parentControl!.SetValue(dropDownProperty, false); + focusParentAction(parentControl!); }); - }); + })); dropDownControl.SetValue(DisposablesProperty, new[] { disposable1, disposable2 }); } diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/FastLineSeparator.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/FastLineSeparator.cs new file mode 100644 index 00000000..bed5768c --- /dev/null +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/FastLineSeparator.cs @@ -0,0 +1,66 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; + +namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers +{ + public class FastLineSeparator : Control + { + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation)); + + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush), Brushes.Black); + + + public FastLineSeparator() + { + AffectsRender(OrientationProperty); + } + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public IBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + protected override Size MeasureOverride(Size availableSize) + { + return Orientation == Orientation.Horizontal + ? new Size(0, 1) + : new Size(1, 0); + } + + protected override Size ArrangeOverride(Size finalSize) + { + return finalSize; + } + + public override void Render(DrawingContext context) + { + var pen = new Pen(Brush); + + if (Orientation == Orientation.Horizontal) + { + // Draw a horizontal line across the control's width + var startPoint = new Point(0, Bounds.Height / 2); + var endPoint = new Point(Bounds.Width - 1, Bounds.Height / 2); + context.DrawLine(pen, startPoint, endPoint); + } + else + { + // Draw a vertical line across the control's height + var startPoint = new Point(Bounds.Width / 2, 0); + var endPoint = new Point(Bounds.Width / 2, Bounds.Height - 1); + context.DrawLine(pen, startPoint, endPoint); + } + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/MenuExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/MenuExtensions.cs index f151f1f6..decb5eec 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/MenuExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/MenuExtensions.cs @@ -5,8 +5,10 @@ using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.LogicalTree; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.VisualTree; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -20,7 +22,7 @@ internal static class MenuExtensions static MenuExtensions() { - FocusOnLoadProperty.Changed.Subscribe(args => + FocusOnLoadProperty.Changed.SubscribeAction(args => { var visual = (Visual)args.Sender; if (args.NewValue.Value) @@ -28,21 +30,23 @@ static MenuExtensions() visual.AttachedToVisualTree += OnAttachedToVisualTree; IDisposable disposable = visual .GetPropertyChangedObservable(InputElement.IsKeyboardFocusWithinProperty) - .Subscribe(eventArgs => + .Subscribe(new AnonymousObserver(eventArgs => { if (!(bool)eventArgs.NewValue!) Dispatcher.UIThread.Post(() => { - var focusedControl = (Control)FocusManager.Instance!.Current; + var focusedControl = + (Control)AvaloniaLocator.Current.GetRequiredService()! + .GetFocusedElement(); var menuItems = visual.GetLogicalAncestors().OfType(); - var focusedTree = focusedControl.GetLogicalAncestors(); + var focusedTree = focusedControl!.GetLogicalAncestors(); foreach (MenuItem menuItem in menuItems.Where(item => !focusedTree.Contains(item)) - .ToArray()) + .ToArray()) menuItem.Close(); }); - }); + })); visual.SetValue(DisposablesProperty, new[] { disposable }); } else diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ScrollViewerExtensions.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ScrollViewerExtensions.cs index a9613694..6e3a3e4c 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ScrollViewerExtensions.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/ScrollViewerExtensions.cs @@ -1,10 +1,10 @@ -using System; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Consolonia.Core.Helpers; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { @@ -19,11 +19,11 @@ internal static class ScrollViewerExtensions static ScrollViewerExtensions() { - ScrollBarsWidthProperty.Changed.Subscribe(args => + ScrollBarsWidthProperty.Changed.SubscribeAction(args => { var scrollViewer = (ScrollViewer)args.Sender; var grid = (Grid)scrollViewer.GetTemplateChildren() - .SingleOrDefault(control => control.Name == @"PART_Root"); + .SingleOrDefault(control => control.Name == "PART_Root"); if (grid != null) { Apply(); @@ -34,13 +34,15 @@ void OnScrollViewerOnTemplateApplied(object sender, TemplateAppliedEventArgs eve { scrollViewer.TemplateApplied -= OnScrollViewerOnTemplateApplied; grid = (Grid)scrollViewer.GetTemplateChildren() - .SingleOrDefault(control => control.Name == @"PART_Root"); + .SingleOrDefault(control => control.Name == "PART_Root"); Apply(); } scrollViewer.TemplateApplied += OnScrollViewerOnTemplateApplied; } + return; + void Apply() { grid.RowDefinitions[1].Height = args.NewValue.Value; @@ -48,7 +50,7 @@ void Apply() } }); - ScrollOnArrowsProperty.Changed.Subscribe(args => + ScrollOnArrowsProperty.Changed.SubscribeAction(args => { var scrollViewer = (ScrollViewer)args.Sender; if (args.NewValue.Value) diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/SymbolsControl.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/SymbolsControl.cs index acfa4b84..cf9b73c0 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/SymbolsControl.cs +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/SymbolsControl.cs @@ -1,14 +1,23 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Consolonia.Core.Text; +using TextShaper = Consolonia.Core.Text.TextShaper; namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers { - public class SymbolsControl : Control + /// + /// Represents a control that displays symbols. + /// This control has two modes of operation depending on the Fill property. + /// If the Fill property is false, it just draws + /// If the Fill property is true, the symbol (Text[0]) is repeated and fills the control. + /// + public sealed class SymbolsControl : Control, IDisposable { /// /// Defines the property. @@ -17,14 +26,15 @@ public class SymbolsControl : Control TextBlock.ForegroundProperty.AddOwner(); public static readonly DirectProperty TextProperty = - TextBlock.TextProperty.AddOwnerWithDataValidation( + AvaloniaProperty.RegisterDirect( + nameof(Text), o => o.Text, (o, v) => o.Text = v, - defaultBindingMode: BindingMode.TwoWay, - enableDataValidation: true); + TextBlock.TextProperty.GetDefaultValue(typeof(TextBlock)), + BindingMode.TwoWay); - private static readonly StyledProperty FillProperty = - AvaloniaProperty.Register("Fill"); + public static readonly StyledProperty FillProperty = + AvaloniaProperty.Register(nameof(Fill)); private GlyphRun _shapedText; @@ -58,12 +68,20 @@ public string Text set { _text = value; - _shapedText = TextShaper.Current.ShapeText( - new ReadOnlySlice((Text ?? string.Empty).ToCharArray()), - Typeface.Default, 1, null); + + _shapedText = new GlyphRun(new GlyphTypeface(), + 1, + (_text ?? string.Empty).AsMemory(), + TextShaper.Convert(_text ?? string.Empty).ToImmutableArray(), + default(Point)); } } + public void Dispose() + { + _shapedText?.Dispose(); + } + public override void Render(DrawingContext context) { if (!Fill) @@ -73,15 +91,20 @@ public override void Render(DrawingContext context) else { var formattedText = new FormattedText( - string.Concat(Enumerable.Repeat(Text?[0] ?? ' ', (int)Bounds.Width)), - Typeface.Default, 1, TextAlignment.Left, TextWrapping.NoWrap, Bounds.Size); - for (int y = 0; y < Bounds.Height; y++) context.DrawText(Foreground, new Point(0, y), formattedText); + string.Concat( + Enumerable.Repeat( + Text?[0] ?? ' ', (int)Bounds.Width)), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + Typeface.Default, 1, Foreground); + for (int y = 0; y < Bounds.Height; y++) + context.DrawText(formattedText, new Point(0, y)); } } protected override Size MeasureOverride(Size availableSize) { - return !Fill ? _shapedText?.Size ?? Size.Empty : Size.Empty; + return !Fill ? _shapedText?.Bounds.Size ?? default : default; } } } \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/TextBoxCaret.cs b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/TextBoxCaret.cs new file mode 100644 index 00000000..9121ac0b --- /dev/null +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/Helpers/TextBoxCaret.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Reactive; +using Consolonia.Core.Drawing; + +namespace Consolonia.Themes.TurboVision.Templates.Controls.Helpers +{ + public class TextBoxCaret : Control + { + public static readonly StyledProperty ActiveProperty = + AvaloniaProperty.Register(nameof(Active)); + + public static readonly StyledProperty PositionProperty = + AvaloniaProperty.Register(nameof(Position)); + + public TextBoxCaret() + { + PositionProperty.Changed.Subscribe(new AnonymousObserver>(args => + { + ((TextBoxCaret)args.Sender).InvalidateVisual(); + })); + + ActiveProperty.Changed.Subscribe(new AnonymousObserver>(args => + { + ((TextBoxCaret)args.Sender).InvalidateVisual(); + })); + + IsHitTestVisible = false; + IsTabStop = false; + Focusable = false; + } + + public bool Active + { + get => GetValue(ActiveProperty); + set => SetValue(ActiveProperty, value); + } + + public Point Position + { + get => GetValue(PositionProperty); + set => SetValue(PositionProperty, value); + } + + public override void Render(DrawingContext context) + { + if (!Active) + return; + + context.DrawLine(new ImmutablePen(new MoveConsoleCaretToPositionBrush()), Position, + Position.WithY(Position.Y + 1)); + } + } +} \ No newline at end of file diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/ItemsControl.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/ItemsControl.axaml index 5d30b9cd..d05eafcf 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/ItemsControl.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/ItemsControl.axaml @@ -7,9 +7,7 @@ BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> + ItemsPanel="{TemplateBinding ItemsPanel}" /> diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBox.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBox.axaml index f5211a5c..3f644cad 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBox.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBox.axaml @@ -30,11 +30,8 @@ + Margin="{TemplateBinding Padding}" /> diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBoxItem.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBoxItem.axaml index 84914484..d53114a8 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBoxItem.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/ListBoxItem.axaml @@ -3,6 +3,9 @@ @@ -144,9 +140,7 @@ BorderThickness="{TemplateBinding BorderThickness}"> diff --git a/src/Consolonia.Themes.TurboVision/Templates/Controls/TabControl.axaml b/src/Consolonia.Themes.TurboVision/Templates/Controls/TabControl.axaml index 83a9e672..d545e3a6 100644 --- a/src/Consolonia.Themes.TurboVision/Templates/Controls/TabControl.axaml +++ b/src/Consolonia.Themes.TurboVision/Templates/Controls/TabControl.axaml @@ -12,9 +12,7 @@ VerticalAlignment="{TemplateBinding VerticalAlignment}"> + ItemsPanel="{TemplateBinding ItemsPanel}" />