diff --git a/README.md b/README.md index 81d758e6..20343c54 100644 --- a/README.md +++ b/README.md @@ -103,17 +103,17 @@ V3 preview: subclassed `SkiaShaderEffect`, implementing `ISkiaGestureProcessor`, ## What's New -### Nuget 1.2.9.2 +### Nuget 1.2.9.4 for SkiaSharp 2.88.9-preview.2.2 -* Reverted SkiaSharp to 2.88.9-preview.2.2 back from 2.88.9 -until [scaling issue](https://github.com/taublast/DrawnUi.Maui/issues/130) is solved for Windows -* Antialiasing [issue](https://github.com/taublast/DrawnUi.Maui/issues/122) solved for SkiaShape -* SkiaImageManager cancelling loads fix -* Fixed native crash when using Super.ReuseBitmaps -* SkiaSvg made more GC-friendly -* Added `WithParent` to FluentExtensions -* Some more +* SkiaShape new Types: Polygon and Line. New property for their Points: Smooth (0-1) to smooth angles. +* Shapes demo page inside SandBox project. +* VisualElement Shadow property now supported everywhere as an optional addition to existing shadows. +* Removed SkiaImage clipping to better support shadows. +* SkiaLabel new property AutoFont: Find and set system font where the first glyph in text is present. Useful for some quick unicode rendering like emoji etc. +* Updated Getsures nuget for correct lock inside MAUI native ScrollView, use Getures="Lock" for Canvas. +* Fixed controls sometimes not invalidated when canvas suface size changes +* Other fixes. ## Development Notes diff --git a/dev/github_uploadnugets.bat b/dev/github_uploadnugets.bat index c7b82b83..b34e6ae1 100644 --- a/dev/github_uploadnugets.bat +++ b/dev/github_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the list of file masks for the packages -set "mask[1]=DrawnUi.Maui*.1.2.9.2*.nupkg" -set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.2*.*nupkg" +set "mask[1]=DrawnUi.Maui*.1.2.9.4*.nupkg" +set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.4*.*nupkg" set "mask_count=2" REM Loop through each file mask diff --git a/dev/nuget_uploadnugets.bat b/dev/nuget_uploadnugets.bat index 43c915a0..f7511333 100644 --- a/dev/nuget_uploadnugets.bat +++ b/dev/nuget_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the list of file masks for the packages -set "mask[1]=DrawnUi.Maui*.1.2.9.2*.nupkg" -set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.2*.*nupkg" +set "mask[1]=DrawnUi.Maui*.1.2.9.4*.nupkg" +set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.4*.*nupkg" set "mask_count=2" REM Loop through each file mask diff --git a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj index 7430a7ef..0b2fb24c 100644 --- a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj +++ b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj @@ -44,7 +44,7 @@ - + \ No newline at end of file diff --git a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj index 8ce580ea..b3f84001 100644 --- a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj +++ b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj index 06ce899e..b43dc124 100644 --- a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj +++ b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj @@ -54,7 +54,7 @@ - + diff --git a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj index 272db962..6db77439 100644 --- a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj +++ b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj index 26973555..be0af46e 100644 --- a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj +++ b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 3d6f4fde..bb1a9409 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ Using SkiaSharp 2.xx. Checkout the DrawnUi Sandbox project for usage example. - 1.2.9.2 + 1.2.9.4 diff --git a/src/Engine/Controls/Carousel/SkiaCarousel.cs b/src/Engine/Controls/Carousel/SkiaCarousel.cs index 2ceadef2..4eeeca00 100644 --- a/src/Engine/Controls/Carousel/SkiaCarousel.cs +++ b/src/Engine/Controls/Carousel/SkiaCarousel.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Collections.Concurrent; +using System.Numerics; using SkiaControl = DrawnUi.Maui.Draw.SkiaControl; namespace DrawnUi.Maui.Controls; @@ -65,7 +66,7 @@ public override void ScrollToNearestAnchor(Vector2 location, Vector2 velocity) public event EventHandler Stopped; - protected Dictionary ItemsVisibility { get; } = new(); + protected ConcurrentDictionary ItemsVisibility { get; } = new(); void SendVisibility(int index, bool state) { @@ -86,6 +87,8 @@ void SendVisibility(int index, bool state) } } + + void InitializeItemsVisibility(int count, bool force) { if (force || ItemsVisibility.Count != count) @@ -1355,93 +1358,93 @@ void ResetPan() { case TouchActionResult.Down: - // if (!IsUserFocused) //first finger down - if (args.Event.NumberOfTouches == 1) //first finger down - { - ResetPan(); - } + // if (!IsUserFocused) //first finger down + if (args.Event.NumberOfTouches == 1) //first finger down + { + ResetPan(); + } - consumed = this; + consumed = this; - break; + break; case TouchActionResult.Panning when args.Event.NumberOfTouches == 1: - if (!IsUserPanning) - { - //first pan - if (args.Event.Distance.Total.X == 0 || Math.Abs(args.Event.Distance.Total.Y) > Math.Abs(args.Event.Distance.Total.X) || Math.Abs(args.Event.Distance.Total.X) < 2) + if (!IsUserPanning) { - return null; + //first pan + if (args.Event.Distance.Total.X == 0 || Math.Abs(args.Event.Distance.Total.Y) > Math.Abs(args.Event.Distance.Total.X) || Math.Abs(args.Event.Distance.Total.X) < 2) + { + return null; + } } - } - if (!IsUserFocused) - { - ResetPan(); - } + if (!IsUserFocused) + { + ResetPan(); + } - //todo add direction - //this.IgnoreWrongDirection + //todo add direction + //this.IgnoreWrongDirection - IsUserPanning = true; + IsUserPanning = true; - var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; - var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; + var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; + var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; - Vector2 velocity; - float useVelocity = 0; - if (!IsVertical) - { - useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); - velocity = new(useVelocity, 0); - } - else - { - useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); - velocity = new(0, useVelocity); - } + Vector2 velocity; + float useVelocity = 0; + if (!IsVertical) + { + useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); + velocity = new(useVelocity, 0); + } + else + { + useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); + velocity = new(0, useVelocity); + } - //record velocity - VelocityAccumulator.CaptureVelocity(velocity); + //record velocity + VelocityAccumulator.CaptureVelocity(velocity); - //saving non clamped - _panningOffset.X = x; - _panningOffset.Y = y; + //saving non clamped + _panningOffset.X = x; + _panningOffset.Y = y; - var clamped = ClampOffset((float)x, (float)y, Bounces); + var clamped = ClampOffset((float)x, (float)y, Bounces); - //Debug.WriteLine($"[CAROUSEL] Panning: {_panningOffset:0} / {clamped:0}"); - ApplyPosition(clamped); + //Debug.WriteLine($"[CAROUSEL] Panning: {_panningOffset:0} / {clamped:0}"); + ApplyPosition(clamped); - consumed = this; - break; + consumed = this; + break; case TouchActionResult.Up: - //Debug.WriteLine($"[Carousel] {args.Type} {IsUserFocused} {IsUserPanning} {InTransition}"); + //Debug.WriteLine($"[Carousel] {args.Type} {IsUserFocused} {IsUserPanning} {InTransition}"); - if (IsUserFocused) - { - - if (IsUserPanning || InTransition) + if (IsUserFocused) { - consumed = this; - var final = VelocityAccumulator.CalculateFinalVelocity(500); + if (IsUserPanning || InTransition) + { + consumed = this; - //animate - CurrentSnap = CurrentPosition; + var final = VelocityAccumulator.CalculateFinalVelocity(500); - ScrollToNearestAnchor(CurrentSnap, final); - } + //animate + CurrentSnap = CurrentPosition; - IsUserPanning = false; - IsUserFocused = false; + ScrollToNearestAnchor(CurrentSnap, final); + } - } + IsUserPanning = false; + IsUserFocused = false; + + } - break; + break; } if (consumed != null || IsUserPanning) @@ -1456,4 +1459,4 @@ void ResetPan() } #endregion -} \ No newline at end of file +} diff --git a/src/Engine/Controls/Slider/SkiaSlider.cs b/src/Engine/Controls/Slider/SkiaSlider.cs index 8fcb97ad..3a9dcf2b 100644 --- a/src/Engine/Controls/Slider/SkiaSlider.cs +++ b/src/Engine/Controls/Slider/SkiaSlider.cs @@ -38,6 +38,8 @@ protected override void OnLayoutChanged() public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) { + //Super.Log($"[Touch] SLIDER got {args.Type}"); + bool passedToChildren = false; ISkiaGestureListener PassToChildren() @@ -183,7 +185,7 @@ void ResetPan() IsUserPanning = true; - //synch this + //synch this if (touchArea == RangeZone.Start) lastTouchX = StartThumbX; else @@ -205,6 +207,8 @@ void ResetPan() { var maybe = lastTouchX + args.Event.Distance.Delta.X / RenderingScale; SetEndOffsetClamped(maybe); + + //Super.Log($"[Touch] SLIDER zone END {maybe}"); } RecalculateValues(); diff --git a/src/Engine/Draw/Base/SkiaControl.Maui.cs b/src/Engine/Draw/Base/SkiaControl.Maui.cs index b792720b..9114b81b 100644 --- a/src/Engine/Draw/Base/SkiaControl.Maui.cs +++ b/src/Engine/Draw/Base/SkiaControl.Maui.cs @@ -22,6 +22,10 @@ public partial class SkiaControl : VisualElement, public static readonly BindableProperty ClearColorProperty = BindableProperty.Create(nameof(ClearColor), typeof(Color), typeof(SkiaControl), Colors.Transparent, propertyChanged: NeedDraw); + + private SkiaShadow platformShadow; + private SKPath platformClip; + public Color ClearColor { get { return (Color)GetValue(ClearColorProperty); } @@ -75,6 +79,16 @@ protected override void OnPropertyChanged([CallerMemberName] string propertyName Update(); } else + if (propertyName == nameof(Shadow)) + { + UpdatePlatformShadow(); + } + else + if (propertyName == nameof(Clip)) + { + Update(); + } + else if (propertyName.IsEither( nameof(Padding), nameof(HorizontalOptions), nameof(VerticalOptions), @@ -287,9 +301,49 @@ public static SKImageFilter CreateShadow(SkiaShadow shadow, float scale) } } + protected void UpdatePlatformShadow() + { + if (this.Shadow != null && Shadow.Brush != null) + { + PlatformShadow = this.Shadow.FromPlatform(); + } + else + { + PlatformShadow = null; + } + } + + protected SkiaShadow PlatformShadow + { + get => platformShadow; + set + { + if (platformShadow != value) + { + platformShadow = value; + OnPropertyChanged(); + } + } + } + + private void GetPlatformClip(SKPath path, SKRect destination, float renderingScale) + { + if (this.Clip != null) + { + this.Clip.FromPlatform(path, destination, renderingScale); + } + } + + protected bool HasPlatformClip() + { + return Clip != null; + } + public static float GetDensity() { return (float)Super.Screen.Density; } + + } } diff --git a/src/Engine/Draw/Base/SkiaControl.Shared.cs b/src/Engine/Draw/Base/SkiaControl.Shared.cs index bec80af1..34242037 100644 --- a/src/Engine/Draw/Base/SkiaControl.Shared.cs +++ b/src/Engine/Draw/Base/SkiaControl.Shared.cs @@ -1612,6 +1612,8 @@ public bool HasFillGradient #endregion + + public virtual SKSize GetSizeRequest(float widthConstraint, float heightConstraint, bool insideLayout) { widthConstraint *= (float)this.WidthRequestRatio; @@ -3836,6 +3838,8 @@ protected virtual ScaledSize SetMeasured(float width, float height, bool widthCu OnMeasured(); + InvalidatedParent = false; + return MeasuredSize; } } @@ -4518,14 +4522,20 @@ public void DrawWithClipAndTransforms( bool useClipping, Action draw) { - bool isClipping = (WillClipBounds || Clipping != null || ClippedBy != null) && useClipping; + bool isClipping = (WillClipBounds || Clipping != null + || ClippedBy != null || HasPlatformClip()) && useClipping; if (isClipping) { - _preparedClipBounds ??= new SKPath(); _preparedClipBounds.Reset(); + + if (HasPlatformClip()) + { + GetPlatformClip(_preparedClipBounds, destination, RenderingScale); + } + else if (ClippedBy != null) { ClippedBy.CreateClip(null, true, _preparedClipBounds); @@ -4628,7 +4638,7 @@ protected virtual void ApplyTransforms(SkiaDrawingContext ctx, SKRect destinatio Helper3d.Reset(); Helper3d.RotateXDegrees((float)RotationX); Helper3d.RotateYDegrees((float)RotationY); - Helper3d.RotateZDegrees((float)RotationZ); + Helper3d.RotateZDegrees(-(float)RotationZ); Helper3d.Translate(0, 0, (float)TranslationZ); drawingMatrix = drawingMatrix.PostConcat(Helper3d.Matrix); @@ -4636,9 +4646,11 @@ protected virtual void ApplyTransforms(SkiaDrawingContext ctx, SKRect destinatio Helper3d.Save(); Helper3d.RotateXDegrees((float)RotationX); Helper3d.RotateYDegrees((float)RotationY); - Helper3d.RotateZDegrees((float)RotationZ); + Helper3d.RotateZDegrees(-(float)RotationZ); Helper3d.TranslateZ((float)TranslationZ); + drawingMatrix = drawingMatrix.PostConcat(Helper3d.Matrix); + Helper3d.Restore(); #endif @@ -4774,54 +4786,26 @@ protected void SafeAction(Action action) Superview.PostponeExecutionAfterDraw(action); } - /* - public async Task ProcessOffscreenCacheRenderingAsync() - { - - await semaphoreOffsecreenProcess.WaitAsync(); - _processingOffscrenRendering = true; - try - { - Action action = _offscreenCacheRenderingQueue.Pop(); - if (!IsDisposed && !IsDisposing && action != null) - { - try - { - action.Invoke(); + protected bool NeedRemeasuring; - RenderObject = RenderObjectPreparing; - _renderObjectPreparing = null; - Repaint(); - } - catch (Exception e) - { - Super.Log(e); - } - } + protected virtual void PaintWithShadows(SkiaDrawingContext ctx, Action render) + { + if (PlatformShadow != null) + { + using var paint = new SKPaint() { IsAntialias = true, FilterQuality = SKFilterQuality.Medium }; + SetupShadow(paint, PlatformShadow, RenderingScale); + var saved = ctx.Canvas.SaveLayer(paint); + render(); + ctx.Canvas.RestoreToCount(saved); } - finally + else { - _processingOffscrenRendering = false; - semaphoreOffsecreenProcess.Release(); - - if (NeedUpdate || _offscreenCacheRenderingQueue.Count > 0) //someone changed us while rendering inner content - { - Update(); //kick - } + render(); } - } - */ - - - protected bool NeedRemeasuring; - - - - protected virtual void PaintWithEffects( @@ -4830,9 +4814,12 @@ protected virtual void PaintWithEffects( if (IsDisposed || IsDisposing) return; - void draw(SkiaDrawingContext context) + void PaintWithEffectsInternal(SkiaDrawingContext context) { - Paint(context, destination, scale, arguments); + PaintWithShadows(context, () => + { + Paint(context, destination, scale, arguments); + }); } if (!DisableEffects && VisualEffects.Count > 0) @@ -4870,7 +4857,7 @@ void draw(SkiaDrawingContext context) { foreach (var effect in renderers) { - var chainedEffectResult = effect.Draw(destination, ctx, draw); + var chainedEffectResult = effect.Draw(destination, ctx, PaintWithEffectsInternal); if (chainedEffectResult.DrawnControl) hasDrawnControl = true; } @@ -4878,14 +4865,14 @@ void draw(SkiaDrawingContext context) if (!hasDrawnControl) { - draw(ctx); + PaintWithEffectsInternal(ctx); } ctx.Canvas.RestoreToCount(restore); } else { - draw(ctx); + PaintWithEffectsInternal(ctx); } } @@ -5882,14 +5869,14 @@ public bool SetupShadow(SKPaint paint, SkiaShadow shadow, float scale) Scale = scale, Shadow = shadow }; - kill?.Dispose(); + DisposeObject(kill); } var old = paint.ImageFilter; paint.ImageFilter = LastShadow.Filter; if (old != paint.ImageFilter) { - old?.Dispose(); + DisposeObject(old); } return true; diff --git a/src/Engine/Draw/Images/SkiaImage.cs b/src/Engine/Draw/Images/SkiaImage.cs index 6a68cb0b..df80e762 100644 --- a/src/Engine/Draw/Images/SkiaImage.cs +++ b/src/Engine/Draw/Images/SkiaImage.cs @@ -69,8 +69,7 @@ public virtual SKImage GetRenderedSource() return null; } - public override bool WillClipBounds => true; - + //public override bool WillClipBounds => true; public CancellationTokenSource CancelLoading; diff --git a/src/Engine/Draw/Images/SkiaImageTiles.cs b/src/Engine/Draw/Images/SkiaImageTiles.cs index b94e60cd..54f2ee9f 100644 --- a/src/Engine/Draw/Images/SkiaImageTiles.cs +++ b/src/Engine/Draw/Images/SkiaImageTiles.cs @@ -125,6 +125,11 @@ public override void OnSourceSuccess() protected virtual SkiaImage CreateTile(double width, double height, LoadedImageSource source) { + if (source != null) + { + source.ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps; + } + var tile = new SkiaImage() { Aspect = this.TileAspect, @@ -290,4 +295,4 @@ public override void OnDisposing() Tile = null; } -} \ No newline at end of file +} diff --git a/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs b/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs index 1adc4fcf..eb26b942 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs @@ -306,10 +306,11 @@ public virtual ScaledSize MeasureStack(SKRect rectForChildrenPixels, float scale var layoutStructure = BuildStackStructure(scale); bool useOneTemplate = + IsTemplated && //ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && RecyclingTemplate == RecyclingTemplate.Enabled; - if (IsTemplated && useOneTemplate) + if (useOneTemplate) { template = ChildrenFactory.GetTemplateInstance(); } @@ -515,7 +516,7 @@ public virtual ScaledSize MeasureStack(SKRect rectForChildrenPixels, float scale secondPass.Scale); } - if (IsTemplated && useOneTemplate) + if (useOneTemplate) { ChildrenFactory.ReleaseView(template); } diff --git a/src/Engine/Draw/Layout/SkiaLayout.Grid.cs b/src/Engine/Draw/Layout/SkiaLayout.Grid.cs index ed611a13..9cab07f0 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.Grid.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.Grid.cs @@ -1,7 +1,7 @@ //Adapted code from the Xamarin.Forms Grid implementation -using DrawnUi.Maui.Infrastructure.Xaml; using System.ComponentModel; +using DrawnUi.Maui.Infrastructure.Xaml; namespace DrawnUi.Maui.Draw; @@ -12,6 +12,7 @@ public partial class SkiaLayout public virtual ScaledSize MeasureGrid(SKRect rectForChildrenPixels, float scale) { + //Trace.WriteLine($"MeasureGrid inside {rectForChildrenPixels}"); var constraints = GetSizeInPoints(rectForChildrenPixels.Size, scale); @@ -65,6 +66,7 @@ protected virtual int DrawChildrenGrid(SkiaDrawingContext context, SKRect destin List tree = new(); + var cellIndex = 0; foreach (var child in cells) { child.OptionalOnBeforeDrawing(); //could set IsVisible or whatever inside @@ -76,6 +78,8 @@ protected virtual int DrawChildrenGrid(SkiaDrawingContext context, SKRect destin var cell = GridStructure.GetCellBoundsFor(child, (destination.Left / scale), (destination.Top / scale)); + //Trace.WriteLine($"cell {cellIndex++} rect {cell}"); + //GetCellBoundsFor is in pixels SKRect cellRect = new((float)Math.Round(cell.Left * scale), (float)Math.Round(cell.Top * scale), (float)Math.Round(cell.Right * scale), (float)Math.Round(cell.Bottom * scale)); @@ -335,4 +339,4 @@ protected void UpdateRowColumnBindingContexts() #endregion -} \ No newline at end of file +} diff --git a/src/Engine/Draw/Scroll/SkiaScroll.cs b/src/Engine/Draw/Scroll/SkiaScroll.cs index 6752ce39..3d18fdf5 100644 --- a/src/Engine/Draw/Scroll/SkiaScroll.cs +++ b/src/Engine/Draw/Scroll/SkiaScroll.cs @@ -1,7 +1,7 @@ -using DrawnUi.Maui.Infrastructure.Helpers; -using System.Numerics; +using System.Numerics; using System.Runtime.CompilerServices; using System.Windows.Input; +using DrawnUi.Maui.Infrastructure.Helpers; namespace DrawnUi.Maui.Draw @@ -471,14 +471,17 @@ public bool ScrollLocked protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) { Vector2 clampedElastic = Vector2.Zero; + var add = Elastic * RenderingScale; bool clamped = false; if (RefreshEnabled) { + add += RefreshDistanceLimit * RenderingScale * 10; + if (Orientation == ScrollOrientation.Vertical && y > 0) //pulling down { clamped = true; - float adjusted = (float)(RefreshIndicator.Height * RenderingScale + 1000); + float adjusted = (float)(RefreshIndicator.Height * RenderingScale + add); var customDims = new Vector2(ContentOffsetBounds.Width, adjusted); clampedElastic = RubberBandUtils.ClampOnTrack( new Vector2(x, y), @@ -491,7 +494,7 @@ protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) if (Orientation == ScrollOrientation.Horizontal && x > 0)//pulling right { clamped = true; - float adjusted = (float)(RefreshIndicator.Width * RenderingScale + 1000); + float adjusted = (float)(RefreshIndicator.Width * RenderingScale + add); var customDims = new Vector2(adjusted, ContentOffsetBounds.Height); clampedElastic = RubberBandUtils.ClampOnTrack( @@ -509,7 +512,8 @@ protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) clampedElastic = RubberBandUtils.ClampOnTrack( new Vector2(x, y), ContentOffsetBounds, - (float)RubberEffect + (float)RubberEffect, + new Vector2(add, add) ); } @@ -528,7 +532,7 @@ protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) return clampedElastic; } - + public static int Elastic = 100; public virtual Vector2 ClampOffset(float x, float y, bool strict = false) { @@ -802,242 +806,242 @@ ISkiaGestureListener PassToChildren() { case TouchActionResult.Tapped: case TouchActionResult.LongPressing: - if (!passedToChildren) - { - _panningStartOffsetPts = new(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); - consumed = PassToChildren(); - } - break; + if (!passedToChildren) + { + _panningStartOffsetPts = new(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); + consumed = PassToChildren(); + } + break; case TouchActionResult.Panning when RespondsToGestures: - bool canPan = !ScrollLocked; - if (Orientation == ScrollOrientation.Vertical) - { - canPan &= Math.Abs(VelocityY) > ScrollVelocityThreshold; - } - else if (Orientation == ScrollOrientation.Horizontal) - { - canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold; - } - else if (Orientation == ScrollOrientation.Both) - { - canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold || Math.Abs(VelocityY) > ScrollVelocityThreshold; - } - - if (lockHeader && !CanScrollUsingHeader) - { - canPan = false; - } - - if (canPan) - { - bool checkOverscroll = true; - - if (!IsUserFocused) + bool canPan = !ScrollLocked; + if (Orientation == ScrollOrientation.Vertical) { - ResetPan(); - checkOverscroll = false; + canPan &= Math.Abs(VelocityY) > ScrollVelocityThreshold; + } + else if (Orientation == ScrollOrientation.Horizontal) + { + canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold; + } + else if (Orientation == ScrollOrientation.Both) + { + canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold || Math.Abs(VelocityY) > ScrollVelocityThreshold; } - IsUserPanning = true; + if (lockHeader && !CanScrollUsingHeader) + { + canPan = false; + } - var movedPtsY = (args.Event.Distance.Delta.Y / RenderingScale) * ChangeDIstancePanned; - var movedPtsX = (args.Event.Distance.Delta.X / RenderingScale) * ChangeDIstancePanned; + if (canPan) + { + bool checkOverscroll = true; - var interpolatedMoveToX = _panningLastDelta.X + (movedPtsX - _panningLastDelta.X) * 0.9f; - var interpolatedMoveToY = _panningLastDelta.Y + (movedPtsY - _panningLastDelta.Y) * 0.9f; + if (!IsUserFocused) + { + ResetPan(); + checkOverscroll = false; + } - _panningLastDelta = new Vector2(interpolatedMoveToX, interpolatedMoveToY); + IsUserPanning = true; - var moveTo = new Vector2( - (float)Math.Round(_panningCurrentOffsetPts.X + interpolatedMoveToX), - (float)Math.Round(_panningCurrentOffsetPts.Y + interpolatedMoveToY)); + var movedPtsY = (args.Event.Distance.Delta.Y / RenderingScale) * ChangeDIstancePanned; + var movedPtsX = (args.Event.Distance.Delta.X / RenderingScale) * ChangeDIstancePanned; - _panningCurrentOffsetPts = moveTo; + var interpolatedMoveToX = _panningLastDelta.X + (movedPtsX - _panningLastDelta.X) * 0.9f; + var interpolatedMoveToY = _panningLastDelta.Y + (movedPtsY - _panningLastDelta.Y) * 0.9f; - if (IgnoreWrongDirection && wrongDirection) - { - IsUserPanning = false; - IsUserFocused = false; - return null; - } + _panningLastDelta = new Vector2(interpolatedMoveToX, interpolatedMoveToY); - VelocityAccumulator.CaptureVelocity(new(VelocityX, VelocityY)); + var moveTo = new Vector2( + (float)Math.Round(_panningCurrentOffsetPts.X + interpolatedMoveToX), + (float)Math.Round(_panningCurrentOffsetPts.Y + interpolatedMoveToY)); - var clamped = ClampOffset(moveTo.X, moveTo.Y); + _panningCurrentOffsetPts = moveTo; - if (!Bounces && checkOverscroll) - { - if (!AreEqual(clamped.X, moveTo.X, 1) && !AreEqual(clamped.Y, moveTo.Y, 1)) + if (IgnoreWrongDirection && wrongDirection) { + IsUserPanning = false; + IsUserFocused = false; return null; } - } - ViewportOffsetX = clamped.X; - ViewportOffsetY = clamped.Y; + VelocityAccumulator.CaptureVelocity(new(VelocityX, VelocityY)); - IsUserPanning = true; - _lastVelocity = new Vector2(VelocityX, VelocityY); + var clamped = ClampOffset(moveTo.X, moveTo.Y); - consumed = this; - } - break; + if (!Bounces && checkOverscroll) + { + if (!AreEqual(clamped.X, moveTo.X, 1) && !AreEqual(clamped.Y, moveTo.Y, 1)) + { + return null; + } + } - case TouchActionResult.Up when RespondsToGestures: - if ((!ChildWasTapped || OverScrolled) && (!ChildWasPanning || IsUserPanning)) - { - if (apply.alreadyConsumed != null) - { - if (CheckNeedToSnap()) - Snap(SystemAnimationTimeSecs); - return null; - } + ViewportOffsetX = clamped.X; + ViewportOffsetY = clamped.Y; - bool canSwipe = true; - if (lockHeader && !CanScrollUsingHeader) - { - canSwipe = false; + IsUserPanning = true; + _lastVelocity = new Vector2(VelocityX, VelocityY); + + consumed = this; } + break; - if (!ScrollLocked && canSwipe) + case TouchActionResult.Up when RespondsToGestures: + if ((!ChildWasTapped || OverScrolled) && (!ChildWasPanning || IsUserPanning)) { - var finalVelocity = VelocityAccumulator.CalculateFinalVelocity(this.MaxVelocity); + if (apply.alreadyConsumed != null) + { + if (CheckNeedToSnap()) + Snap(SystemAnimationTimeSecs); + return null; + } - bool fling = false; - bool swipe = false; + bool canSwipe = true; + if (lockHeader && !CanScrollUsingHeader) + { + canSwipe = false; + } - if (!OverScrolled || Orientation == ScrollOrientation.Both) + if (!ScrollLocked && canSwipe) { - var mainDirection = GetDirectionType(new Vector2(finalVelocity.X, finalVelocity.Y), DirectionType.None, 0.9f); + var finalVelocity = VelocityAccumulator.CalculateFinalVelocity(this.MaxVelocity); - if (Orientation != ScrollOrientation.Both && !IsUserPanning) + bool fling = false; + bool swipe = false; + + if (!OverScrolled || Orientation == ScrollOrientation.Both) { - if (IgnoreWrongDirection) + var mainDirection = GetDirectionType(new Vector2(finalVelocity.X, finalVelocity.Y), DirectionType.None, 0.9f); + + if (Orientation != ScrollOrientation.Both && !IsUserPanning) { - if (Orientation == ScrollOrientation.Vertical && mainDirection != DirectionType.Vertical) - { - return null; - } - if (Orientation == ScrollOrientation.Horizontal && mainDirection != DirectionType.Horizontal) + if (IgnoreWrongDirection) { - return null; + if (Orientation == ScrollOrientation.Vertical && mainDirection != DirectionType.Vertical) + { + return null; + } + if (Orientation == ScrollOrientation.Horizontal && mainDirection != DirectionType.Horizontal) + { + return null; + } } } + + var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; + if ((Orientation == ScrollOrientation.Both + || (Orientation == ScrollOrientation.Vertical && mainDirection == DirectionType.Vertical) + || (Orientation == ScrollOrientation.Horizontal && mainDirection == DirectionType.Horizontal)) + && (Math.Abs(finalVelocity.X) > swipeThreshold || Math.Abs(finalVelocity.Y) > swipeThreshold)) + { + swipe = true; + } } - var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; - if ((Orientation == ScrollOrientation.Both - || (Orientation == ScrollOrientation.Vertical && mainDirection == DirectionType.Vertical) - || (Orientation == ScrollOrientation.Horizontal && mainDirection == DirectionType.Horizontal)) - && (Math.Abs(finalVelocity.X) > swipeThreshold || Math.Abs(finalVelocity.Y) > swipeThreshold)) + if (OverScrolled || swipe) { - swipe = true; - } - } + IsUserPanning = false; - if (OverScrolled || swipe) - { - IsUserPanning = false; + bool bounceX = false, bounceY = false; + if (OverScrolled) + { + var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); + var closestPoint = GetClosestSidePoint(new SKPoint((float)InternalViewportOffset.Units.X, (float)InternalViewportOffset.Units.Y), contentRect, Viewport.Units.Size); - bool bounceX = false, bounceY = false; - if (OverScrolled) - { - var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); - var closestPoint = GetClosestSidePoint(new SKPoint((float)InternalViewportOffset.Units.X, (float)InternalViewportOffset.Units.Y), contentRect, Viewport.Units.Size); + var axis = new Vector2(closestPoint.X, closestPoint.Y); - var axis = new Vector2(closestPoint.X, closestPoint.Y); + var velocityY = finalVelocity.Y * ChangeVelocityScrolled; + var velocityX = finalVelocity.X * ChangeVelocityScrolled; - var velocityY = finalVelocity.Y * ChangeVelocityScrolled; - var velocityX = finalVelocity.X * ChangeVelocityScrolled; + if (Math.Abs(velocityX) > MaxBounceVelocity) + { + velocityX = Math.Sign(velocityX) * MaxBounceVelocity; + } - if (Math.Abs(velocityX) > MaxBounceVelocity) - { - velocityX = Math.Sign(velocityX) * MaxBounceVelocity; - } + if (Math.Abs(velocityY) > MaxBounceVelocity) + { + velocityY = Math.Sign(velocityY) * MaxBounceVelocity; + } - if (Math.Abs(velocityY) > MaxBounceVelocity) - { - velocityY = Math.Sign(velocityY) * MaxBounceVelocity; - } + if (OverscrollDistance.Y != 0) + { + bounceY = true; + BounceY(InternalViewportOffset.Units.Y, + axis.Y, velocityY); + } - if (OverscrollDistance.Y != 0) - { - bounceY = true; - BounceY(InternalViewportOffset.Units.Y, - axis.Y, velocityY); - } + if (OverscrollDistance.X != 0) + { + bounceX = true; + BounceX(InternalViewportOffset.Units.X, + axis.X, velocityX); + } - if (OverscrollDistance.X != 0) - { - bounceX = true; - BounceX(InternalViewportOffset.Units.X, - axis.X, velocityX); + fling = true; } - fling = true; - } - - if (Orientation != ScrollOrientation.Neither) - { - if (!Bounces) + if (Orientation != ScrollOrientation.Neither) { - if (Orientation == ScrollOrientation.Vertical && !bounceY) + if (!Bounces) { - if ((AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Bottom, 1) && finalVelocity.Y > 0) || - (AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Top, 1) && finalVelocity.Y < 0)) - return null; + if (Orientation == ScrollOrientation.Vertical && !bounceY) + { + if ((AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Bottom, 1) && finalVelocity.Y > 0) || + (AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Top, 1) && finalVelocity.Y < 0)) + return null; + } + if (Orientation == ScrollOrientation.Horizontal && !bounceX) + { + if ((AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Right, 1) && finalVelocity.X > 0) || + (AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Left, 1) && finalVelocity.X < 0)) + return null; + } } - if (Orientation == ScrollOrientation.Horizontal && !bounceX) + + var velocityY = finalVelocity.Y * ChangeVelocityScrolled; + var velocityX = finalVelocity.X * ChangeVelocityScrolled; + + if (Math.Abs(velocityX) > _minVelocity && !bounceX) { - if ((AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Right, 1) && finalVelocity.X > 0) || - (AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Left, 1) && finalVelocity.X < 0)) - return null; + IsUserFocused = false; + _vectorAnimatorBounceX.Stop(); + fling = StartToFlingFrom(_animatorFlingX, ViewportOffsetX, velocityX); } - } - var velocityY = finalVelocity.Y * ChangeVelocityScrolled; - var velocityX = finalVelocity.X * ChangeVelocityScrolled; - - if (Math.Abs(velocityX) > _minVelocity && !bounceX) - { - IsUserFocused = false; - _vectorAnimatorBounceX.Stop(); - fling = StartToFlingFrom(_animatorFlingX, ViewportOffsetX, velocityX); + if (Math.Abs(velocityY) > _minVelocity && !bounceY) + { + IsUserFocused = false; + _vectorAnimatorBounceY.Stop(); + fling = StartToFlingFrom(_animatorFlingY, ViewportOffsetY, velocityY); + } } - if (Math.Abs(velocityY) > _minVelocity && !bounceY) + if (fling) { - IsUserFocused = false; - _vectorAnimatorBounceY.Stop(); - fling = StartToFlingFrom(_animatorFlingY, ViewportOffsetY, velocityY); + WasSwiping = true; + consumed = this; + passedToChildren = true; } } - if (fling) - { - WasSwiping = true; - consumed = this; - passedToChildren = true; - } - } - - IsUserFocused = false; - IsUserPanning = false; + IsUserFocused = false; + IsUserPanning = false; - if (!fling) - { - if (CheckNeedToSnap()) - Snap(SystemAnimationTimeSecs); - else + if (!fling) { - _destination = SKRect.Empty; + if (CheckNeedToSnap()) + Snap(SystemAnimationTimeSecs); + else + { + _destination = SKRect.Empty; + } } } + break; } break; - } - break; } } @@ -1127,14 +1131,14 @@ void UpdateLoadingLock(Vector2 velocity) switch (Orientation) { case ScrollOrientation.Vertical: - shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock; - break; + shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock; + break; case ScrollOrientation.Horizontal: - shouldLock = Math.Abs(velocity.X) >= VelocityImageLoaderLock; - break; + shouldLock = Math.Abs(velocity.X) >= VelocityImageLoaderLock; + break; default: - shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock || Math.Abs(velocity.X) >= VelocityImageLoaderLock; - break; + shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock || Math.Abs(velocity.X) >= VelocityImageLoaderLock; + break; } UpdateLoadingLock(shouldLock); diff --git a/src/Engine/Draw/SkiaPoint.cs b/src/Engine/Draw/SkiaPoint.cs new file mode 100644 index 00000000..314c5200 --- /dev/null +++ b/src/Engine/Draw/SkiaPoint.cs @@ -0,0 +1,52 @@ +namespace DrawnUi.Maui.Draw +{ + public class SkiaPoint : BindableObject + { + public SkiaPoint() + { + + } + + public SkiaPoint(double x, double y) + { + X = x; + Y = y; + } + + public static readonly BindableProperty XProperty = BindableProperty.Create( + nameof(X), + typeof(double), + typeof(SkiaPoint), + 0.0, + propertyChanged: OnPointPropertyChanged); + + public double X + { + get => (double)GetValue(XProperty); + set => SetValue(XProperty, value); + } + + public static readonly BindableProperty YProperty = BindableProperty.Create( + nameof(Y), + typeof(double), + typeof(SkiaPoint), + 0.0, + propertyChanged: OnPointPropertyChanged); + + public double Y + { + get => (double)GetValue(YProperty); + set => SetValue(YProperty, value); + } + + private static void OnPointPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SkiaPoint point) + { + point.ParentShape?.Update(); + } + } + + internal SkiaShape ParentShape { get; set; } + } +} diff --git a/src/Engine/Draw/SkiaShape.cs b/src/Engine/Draw/SkiaShape.cs index c07edbf2..4b601489 100644 --- a/src/Engine/Draw/SkiaShape.cs +++ b/src/Engine/Draw/SkiaShape.cs @@ -1,5 +1,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; +using DrawnUi.Maui.Infrastructure.Xaml; namespace DrawnUi.Maui.Draw @@ -138,6 +140,9 @@ public double StrokeWidth SKStrokeCap.Round, propertyChanged: NeedDraw); + /// + /// Affects joint of shapes. Default is KStrokeCap.Round + /// public SKStrokeCap StrokeCap { get { return (SKStrokeCap)GetValue(StrokeCapProperty); } @@ -145,8 +150,6 @@ public SKStrokeCap StrokeCap } - - public static readonly BindableProperty LayoutChildrenProperty = BindableProperty.Create( nameof(LayoutChildren), typeof(LayoutType), @@ -166,6 +169,9 @@ public LayoutType LayoutChildren SKBlendMode.SrcOver, propertyChanged: NeedDraw); + /// + /// Default is SKBlendMode.SrcOver + /// public SKBlendMode StrokeBlendMode { get { return (SKBlendMode)GetValue(StrokeBlendModeProperty); } @@ -265,8 +271,7 @@ protected void CalculateSizeForStroke(SKRect destination, float scale) strokeAwareSize = SKRect.Inflate(strokeAwareSize, -halfStroke, -halfStroke); - strokeAwareChildrenSize = - SKRect.Inflate(strokeAwareSize, -halfStroke, -halfStroke); + strokeAwareChildrenSize = strokeAwareSize; } MeasuredStrokeAwareSize = strokeAwareSize; @@ -387,10 +392,10 @@ public override SKPath CreateClip(object arguments, bool usePosition, SKPath pat case ShapeType.Circle: ShouldClipAntialiased = true; path.AddCircle( - (float)Math.Round(strokeAwareChildrenSize.Left + strokeAwareChildrenSize.Width / 2.0f), - (float)Math.Round(strokeAwareChildrenSize.Top + strokeAwareChildrenSize.Height / 2.0f), - Math.Min(strokeAwareChildrenSize.Width, strokeAwareChildrenSize.Height) / - 2.0f); + (float)(strokeAwareChildrenSize.Left + strokeAwareChildrenSize.Width / 2.0f), + (float)(strokeAwareChildrenSize.Top + strokeAwareChildrenSize.Height / 2.0f), + (float)Math.Floor(Math.Min(strokeAwareChildrenSize.Width, strokeAwareChildrenSize.Height) / + 2.0f) + 0); break; case ShapeType.Ellipse: @@ -399,6 +404,37 @@ public override SKPath CreateClip(object arguments, bool usePosition, SKPath pat path.AddOval(strokeAwareChildrenSize); break; + case ShapeType.Polygon: + if (Points != null && Points.Count > 0) + { + path.Reset(); + + var scaleX = MeasuredStrokeAwareSize.Width; + var scaleY = MeasuredStrokeAwareSize.Height; + var offsetX = usePosition ? MeasuredStrokeAwareSize.Left : 0; + var offsetY = usePosition ? MeasuredStrokeAwareSize.Top : 0; + + bool first = true; + foreach (var skiaPoint in Points) + { + var point = new SKPoint( + (float)Math.Round(offsetX + skiaPoint.X * scaleX), + (float)Math.Round(offsetY + skiaPoint.Y * scaleY)); + + if (first) + { + path.MoveTo(point); + first = false; + } + else + { + path.LineTo(point); + } + } + path.Close(); + } + break; + case ShapeType.Rectangle: default: if (CornerRadius != default) @@ -476,6 +512,27 @@ protected virtual void PaintBackground(SkiaDrawingContext ctx, break; + case ShapeType.Polygon: + + if (Points != null && Points.Count > 1) + { + DrawPathShape.Reset(); + + paint.StrokeJoin = MapStrokeCapToStrokeJoin(this.StrokeCap); + + if (SmoothPoints > 0) + { + AddSmoothPath(DrawPathShape, Points, MeasuredStrokeAwareSize, SmoothPoints, true); + } + else + { + AddStraightPath(DrawPathShape, Points, MeasuredStrokeAwareSize, true); + } + + ctx.Canvas.DrawPath(DrawPathShape, paint); + } + break; + case ShapeType.Circle: if (StrokeWidth == 0 || StrokeColor == TransparentColor) { @@ -512,6 +569,24 @@ protected virtual void PaintBackground(SkiaDrawingContext ctx, } } + protected virtual SKStrokeJoin MapStrokeCapToStrokeJoin(SKStrokeCap strokeCap) + { + switch (strokeCap) + { + case SKStrokeCap.Round: + return SKStrokeJoin.Round; + case SKStrokeCap.Square: + return SKStrokeJoin.Bevel; + case SKStrokeCap.Butt: + default: + return SKStrokeJoin.Miter; + } + } + + protected override void PaintWithShadows(SkiaDrawingContext ctx, Action render) + { + render(); //we will handle shadows by ourselves + } protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) { @@ -533,7 +608,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float RenderingPaint ??= new SKPaint() { - IsAntialias = true + IsAntialias = true, }; RenderingPaint.IsDither = IsDistorted; @@ -590,7 +665,8 @@ void PaintStroke(SKPaint paint) } paint.Style = SKPaintStyle.Stroke; - paint.StrokeCap = this.StrokeCap; //todo full stroke object + paint.StrokeCap = this.StrokeCap; + paint.StrokeJoin = MapStrokeCapToStrokeJoin(this.StrokeCap); if (this.StrokePath != null && StrokePath.Length > 0) { @@ -625,6 +701,22 @@ void PaintStroke(SKPaint paint) ctx.Canvas.DrawCircle(outRect.MidX, outRect.MidY, minSize / 2.0f, paint); break; + case ShapeType.Line: + if (Points != null && Points.Count > 1) + { + DrawPathShape.Reset(); + if (SmoothPoints > 0) + { + AddSmoothPath(DrawPathShape, Points, strokeAwareSize, SmoothPoints, false); + } + else + { + AddStraightPath(DrawPathShape, Points, strokeAwareSize, false); + } + ctx.Canvas.DrawPath(DrawPathShape, paint); + } + break; + case ShapeType.Ellipse: DrawPathShape.Reset(); DrawPathShape.AddOval(outRect); @@ -646,35 +738,66 @@ void PaintStroke(SKPaint paint) ctx.Canvas.DrawPath(DrawPathShape, paint); break; + + case ShapeType.Polygon: + if (Points != null && Points.Count > 1) + { + DrawPathShape.Reset(); + + var path = DrawPathShape; + + if (SmoothPoints > 0) + { + AddSmoothPath(path, Points, strokeAwareSize, SmoothPoints, true); + } + else + { + AddStraightPath(path, Points, strokeAwareSize, true); + } + + ctx.Canvas.DrawPath(path, RenderingPaint); + } + break; } } - void PaintWithShadows(Action render) + void PaintWithShadowsInternal(Action render) { - if (Shadows != null && Shadows.Count > 0) + + void RenderShadow(SkiaShadow shadow) { - for (int index = 0; index < Shadows.Count(); index++) + SetupShadow(RenderingPaint, shadow, RenderingScale); + + if (ClipBackgroundColor) { - SetupShadow(RenderingPaint, Shadows[index], RenderingScale); + ClipContentPath ??= new(); + ClipContentPath.Reset(); + CreateClip(arguments, true, ClipContentPath); - if (ClipBackgroundColor) - { - ClipContentPath ??= new(); - ClipContentPath.Reset(); - CreateClip(arguments, true, ClipContentPath); + var saved = ctx.Canvas.Save(); - var saved = ctx.Canvas.Save(); + ClipSmart(ctx.Canvas, ClipContentPath, SKClipOperation.Difference); + render(); - ClipSmart(ctx.Canvas, ClipContentPath, SKClipOperation.Difference); - render(); + ctx.Canvas.RestoreToCount(saved); + } + else + { + render(); + } + } - ctx.Canvas.RestoreToCount(saved); - } - else - { - render(); - } + if (PlatformShadow != null) + { + RenderShadow(PlatformShadow); + } + else + if (Shadows != null && Shadows.Count > 0) + { + for (int index = 0; index < Shadows.Count(); index++) + { + RenderShadow(Shadows[index]); } } else @@ -684,7 +807,7 @@ void PaintWithShadows(Action render) } //background with shadows pass, no stroke - PaintWithShadows(() => + PaintWithShadowsInternal(() => { if (SetupBackgroundPaint(RenderingPaint, outRect)) { @@ -716,8 +839,6 @@ void PaintWithShadows(Action render) } - - #endregion #region SHADOWS @@ -792,7 +913,7 @@ private void OnShadowCollectionChanged(object sender, NotifyCollectionChangedEve typeof(SkiaShape), defaultValueCreator: (instance) => { - var created = new ObservableCollection(); + var created = new SkiaShadowsCollection(); ShadowsPropertyChanged(instance, null, created); return created; }, @@ -822,6 +943,371 @@ private static object CoerceShadows(BindableObject bindable, object value) #endregion + #region POINTS + + private static object CoercePoints(BindableObject bindable, object value) + { + if (value is ReadOnlyCollection readonlyCollection) + { + return new ReadOnlyCollection(readonlyCollection.ToList()); + } + + return value; + } + + + public static readonly BindableProperty PointsProperty = BindableProperty.Create( + nameof(Points), + typeof(IList), + typeof(SkiaShape), + defaultValueCreator: (instance) => + { + var created = new ObservableCollection(); + PointsPropertyChanged(instance, null, created); + return created; + }, + validateValue: (bo, v) => v is IList, + propertyChanged: NeedDraw, + coerceValue: CoercePoints); + + [TypeConverter(typeof(SkiaPointCollectionConverter))] + public IList Points + { + get => (IList)GetValue(PointsProperty); + set => SetValue(PointsProperty, value); + } + + public static List PolygonStar + { + get + { + return CreateStarPoints(5); + } + } + + private static void PointsPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SkiaShape control) + { + if (oldValue is INotifyCollectionChanged oldCollection) + { + oldCollection.CollectionChanged -= control.OnPointsCollectionChanged; + } + + if (oldValue is IEnumerable oldPoints) + { + foreach (var point in oldPoints) + { + point.ParentShape = null; + } + } + + if (newValue is IEnumerable newPoints) + { + foreach (var point in newPoints) + { + point.ParentShape = control; + } + } + + if (newValue is INotifyCollectionChanged newCollection) + { + newCollection.CollectionChanged -= control.OnPointsCollectionChanged; + newCollection.CollectionChanged += control.OnPointsCollectionChanged; + } + + control.Update(); + } + } + + private void OnPointsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + foreach (SkiaPoint oldPoint in e.OldItems) + { + oldPoint.ParentShape = null; + } + } + + if (e.NewItems != null) + { + foreach (SkiaPoint newPoint in e.NewItems) + { + newPoint.ParentShape = this; + } + } + + Update(); + } + + + + #endregion + + #region SMOOTH + + public static readonly BindableProperty SmoothPointsProperty = BindableProperty.Create( + nameof(SmoothPoints), + typeof(float), + typeof(SkiaShape), + 0f, // Default value is 0 (no smoothing) + propertyChanged: NeedDraw, + coerceValue: (bindable, value) => + { + float val = (float)value; + return Math.Max(0f, Math.Min(1f, val)); // Clamp between 0 and 1 + } + ); + + /// + /// Controls the automatic smoothness between points of Line and Polygon. Ranges from 0.0 (no smoothing) to 1.0. + /// Works for sequential points only. + /// + public float SmoothPoints + { + get => (float)GetValue(SmoothPointsProperty); + set => SetValue(SmoothPointsProperty, value); + } + + private void AddSmoothPath(SKPath path, IList points, SKRect rect, float smoothness, bool isClosed) + { + if (points == null || points.Count < 2) + { + return; + } + + var scaledPoints = points.Select(p => ScalePoint(p, rect)).ToList(); + + if (isClosed) + { + var firstPoint = scaledPoints[0]; + var closingPoint = new SKPoint( + (firstPoint.X + scaledPoints[1].X) / 2, + (firstPoint.Y + scaledPoints[1].Y) / 2); + + scaledPoints.RemoveAt(0); + scaledPoints.Insert(0, closingPoint); + scaledPoints.Add(firstPoint); + } + + path.MoveTo(scaledPoints[0]); + + int pointCount = scaledPoints.Count; + + if (isClosed) + { + for (int i = 0; i < pointCount; i++) + { + SmoothPoint(path, scaledPoints, i, smoothness, isClosed); + } + path.Close(); + } + else + { + for (int i = 0; i < pointCount; i++) + { + if (i == 0 || i == pointCount - 1) + { + path.LineTo(scaledPoints[i]); + } + else + { + SmoothPoint(path, scaledPoints, i, smoothness, isClosed); + } + } + } + } + + private void SmoothPoint(SKPath path, IList scaledPoints, int i, float smoothness, bool isClosed) + { + int pointCount = scaledPoints.Count; + + var prev = scaledPoints[(i - 1 + pointCount) % pointCount]; + var current = scaledPoints[i]; + var next = scaledPoints[(i + 1) % pointCount]; + + if (!isClosed) + { + if (i == 0) + { + path.LineTo(current); + return; + } + if (i == pointCount - 1) + { + path.LineTo(current); + return; + } + } + + var v1 = new SKPoint(current.X - prev.X, current.Y - prev.Y); + var v2 = new SKPoint(next.X - current.X, next.Y - current.Y); + + float lengthV1 = (float)Math.Sqrt(v1.X * v1.X + v1.Y * v1.Y); + float lengthV2 = (float)Math.Sqrt(v2.X * v2.X + v2.Y * v2.Y); + + if (lengthV1 == 0 || lengthV2 == 0) + { + path.LineTo(current); + return; + } + + v1 = new SKPoint(v1.X / lengthV1, v1.Y / lengthV1); + v2 = new SKPoint(v2.X / lengthV2, v2.Y / lengthV2); + + float smoothingRadius = lengthV1 * smoothness * 0.3f; + smoothingRadius = Math.Min(smoothingRadius, lengthV1 * 0.5f); + smoothingRadius = Math.Min(smoothingRadius, lengthV2 * 0.5f); + + if (smoothingRadius < 0.001f) + { + path.LineTo(current); // No significant smoothing is needed + return; + } + + var p1 = new SKPoint( + current.X - v1.X * smoothingRadius, + current.Y - v1.Y * smoothingRadius); + + var p2 = new SKPoint( + current.X + v2.X * smoothingRadius, + current.Y + v2.Y * smoothingRadius); + + path.LineTo(p1); + + // quadratic Bezier curve to smooth angle + path.QuadTo(current, p2); + } + + private void AddStraightPath(SKPath path, IList points, SKRect rect, bool isClosed) + { + if (points == null || points.Count < 2) + { + return; + } + + path.MoveTo(ScalePoint(points[0], rect)); + + for (int i = 1; i < points.Count; i++) + { + path.LineTo(ScalePoint(points[i], rect)); + } + + if (isClosed) + { + path.Close(); + } + } + + private SKPoint ScalePoint(SkiaPoint point, SKRect rect) + { + return new SKPoint( + (float)Math.Round(rect.Left + point.X * rect.Width), + (float)Math.Round(rect.Top + point.Y * rect.Height)); + } + + #endregion + + public static List CreateStarPoints(int numberOfPoints, double innerRadiusRatio = 0.5) + { + if (numberOfPoints < 2) + throw new ArgumentException("Number of points must be at least 2.", nameof(numberOfPoints)); + if (innerRadiusRatio <= 0 || innerRadiusRatio >= 1) + throw new ArgumentException("Inner radius ratio must be between 0 and 1.", nameof(innerRadiusRatio)); + + List points = new List(); + double angleStep = Math.PI / numberOfPoints; + double outerRadius = 1.0; + double innerRadius = outerRadius * innerRadiusRatio; + + for (int i = 0; i < numberOfPoints * 2; i++) + { + double angle = i * angleStep - Math.PI / 2; + + double radius = (i % 2 == 0) ? outerRadius : innerRadius; + double x = radius * Math.Cos(angle); + double y = radius * Math.Sin(angle); + + points.Add(new SkiaPoint((float)x, (float)y)); + } + + // Find bounding box + var minX = points.Min(p => p.X); + var maxX = points.Max(p => p.X); + var minY = points.Min(p => p.Y); + var maxY = points.Max(p => p.Y); + + var scaleX = 1.0f / (maxX - minX); + var scaleY = 1.0f / (maxY - minY); + + var scale = Math.Min(scaleX, scaleY); + + var offsetX = (1.0f - (maxX - minX) * scale) / 2.0f - minX * scale; + var offsetY = (1.0f - (maxY - minY) * scale) / 2.0f - minY * scale; + + for (int i = 0; i < points.Count; i++) + { + var x = points[i].X * scale + offsetX; + var y = points[i].Y * scale + offsetY; + + points[i] = new SkiaPoint(x, y); + } + + return points; + } + + public static List CreateStarPointsCrossed(int numberOfPoints) + { + if (numberOfPoints < 5 || numberOfPoints % 2 == 0) + throw new ArgumentException("Number of points must be an odd number greater than or equal to 5.", nameof(numberOfPoints)); + + List points = new List(); + double angleStep = 2 * Math.PI / numberOfPoints; + double radius = 1.0; + + List outerPoints = new List(); + for (int i = 0; i < numberOfPoints; i++) + { + double angle = i * angleStep - Math.PI / 2; + double x = radius * Math.Cos(angle); + double y = radius * Math.Sin(angle); + outerPoints.Add(new SkiaPoint((float)x, (float)y)); + } + + int skip = (numberOfPoints - 1) / 2; + for (int i = 0; i < numberOfPoints; i++) + { + points.Add(outerPoints[(i * skip) % numberOfPoints]); + } + + points.Add(points[0]); + + // Find bounding box + var minX = points.Min(p => p.X); + var maxX = points.Max(p => p.X); + var minY = points.Min(p => p.Y); + var maxY = points.Max(p => p.Y); + + var scaleX = 1.0f / (maxX - minX); + var scaleY = 1.0f / (maxY - minY); + + var scale = Math.Min(scaleX, scaleY); + + var offsetX = (1.0f - (maxX - minX) * scale) / 2.0f - minX * scale; + var offsetY = (1.0f - (maxY - minY) * scale) / 2.0f - minY * scale; + + for (int i = 0; i < points.Count; i++) + { + var x = points[i].X * scale + offsetX; + var y = points[i].Y * scale + offsetY; + + points[i] = new SkiaPoint(x, y); + } + + return points; + } + } } diff --git a/src/Engine/Draw/Text/SkiaLabel.cs b/src/Engine/Draw/Text/SkiaLabel.cs index 2ad1dee5..16642254 100644 --- a/src/Engine/Draw/Text/SkiaLabel.cs +++ b/src/Engine/Draw/Text/SkiaLabel.cs @@ -313,9 +313,6 @@ public void MergeSpansForLines( public List Glyphs { get; protected set; } = new(); - //todo - bool AutoFindFont = false; - public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) { if (IsDisposed || IsDisposing) @@ -369,20 +366,25 @@ public override ScaledSize Measure(float widthConstraint, float heightConstraint Glyphs = GetGlyphs(Text, PaintDefault.Typeface); - if (AutoFindFont) + if (AutoFont) { if (Glyphs != null && Glyphs.Count > 0) { - if (UnicodeNeedsShaping(Glyphs[0].Symbol)) + var first = Glyphs[0].Symbol; + var typeFace = SkiaFontManager.Manager.MatchCharacter(first); + if (typeFace != null) { - needsShaping = true; + //FontDetectedWith = glyph.Symbol; + needsShaping = SkiaLabel.UnicodeNeedsShaping(first); + _replaceFont = typeFace; + ReplaceFont(); } } text = Text; } else { - //replace unprintable symbols + //replace unprintable symbols with fallback if (Glyphs.Count > 0) { var textFiltered = ""; @@ -1438,6 +1440,8 @@ protected virtual void UpdateFont() } + + protected void ReplaceFont() { var newFont = _replaceFont; @@ -2676,6 +2680,21 @@ public int FontWeight set { SetValue(FontWeightProperty, value); } } + public static readonly BindableProperty AutoFontProperty = BindableProperty.Create( + nameof(AutoFont), + typeof(bool), + typeof(SkiaLabel), + false, propertyChanged: NeedUpdateFont); + + /// + /// Find and set system font where the first glyph in text is present + /// + public bool AutoFont + { + get { return (bool)GetValue(AutoFontProperty); } + set { SetValue(AutoFontProperty, value); } + } + public static readonly BindableProperty TypeFaceProperty = BindableProperty.Create( nameof(TypeFace), typeof(SKTypeface), diff --git a/src/Engine/DrawnUi.Maui.csproj b/src/Engine/DrawnUi.Maui.csproj index 304e08e8..c503e4ba 100644 --- a/src/Engine/DrawnUi.Maui.csproj +++ b/src/Engine/DrawnUi.Maui.csproj @@ -9,7 +9,6 @@ true - 14.2 14.0 21.0 @@ -62,6 +61,7 @@ + @@ -92,11 +92,12 @@ + - + @@ -104,38 +105,39 @@ - - + - + + + + + + + - - + @@ -163,4 +165,6 @@ + + diff --git a/src/Engine/Features/Effects/ChainDropShadowsEffect.cs b/src/Engine/Features/Effects/ChainDropShadowsEffect.cs index 8a707835..c49e84f4 100644 --- a/src/Engine/Features/Effects/ChainDropShadowsEffect.cs +++ b/src/Engine/Features/Effects/ChainDropShadowsEffect.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; +using DrawnUi.Maui.Infrastructure.Xaml; namespace DrawnUi.Maui.Draw; @@ -14,7 +15,7 @@ public class ChainDropShadowsEffect : BaseChainedEffect typeof(ChainDropShadowsEffect), defaultValueCreator: (instance) => { - var created = new ObservableCollection(); + var created = new SkiaShadowsCollection(); ShadowsPropertyChanged(instance, null, created); return created; }, @@ -79,22 +80,22 @@ private void OnShadowCollectionChanged(object sender, NotifyCollectionChangedEve { switch (e.Action) { - case NotifyCollectionChangedAction.Add: - foreach (SkiaShadow newSkiaPropertyShadow in e.NewItems) - { - newSkiaPropertyShadow.Attach(this); - } + case NotifyCollectionChangedAction.Add: + foreach (SkiaShadow newSkiaPropertyShadow in e.NewItems) + { + newSkiaPropertyShadow.Attach(this); + } - break; + break; - case NotifyCollectionChangedAction.Reset: - case NotifyCollectionChangedAction.Remove: - foreach (SkiaShadow oldSkiaPropertyShadow in e.OldItems ?? new SkiaShadow[0]) - { - oldSkiaPropertyShadow.Dettach(); - } + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Remove: + foreach (SkiaShadow oldSkiaPropertyShadow in e.OldItems ?? new SkiaShadow[0]) + { + oldSkiaPropertyShadow.Dettach(); + } - break; + break; } Update(); @@ -154,4 +155,4 @@ public override bool NeedApply return base.NeedApply && Shadows.Count > 0; } } -} \ No newline at end of file +} diff --git a/src/Engine/Features/Gestures/GesturesMode.cs b/src/Engine/Features/Gestures/GesturesMode.cs index bda7b733..9ec77da0 100644 --- a/src/Engine/Features/Gestures/GesturesMode.cs +++ b/src/Engine/Features/Gestures/GesturesMode.cs @@ -19,9 +19,4 @@ public enum GesturesMode /// Lock input for self, useful inside scroll view, panning controls like slider etc /// Lock, - - /// - /// Tries to let other views consume the touch event if this view doesn't handle it - /// - Share, -} \ No newline at end of file +} diff --git a/src/Engine/Internals/Enums/ShapeType.cs b/src/Engine/Internals/Enums/ShapeType.cs index af07f339..78aeb64d 100644 --- a/src/Engine/Internals/Enums/ShapeType.cs +++ b/src/Engine/Internals/Enums/ShapeType.cs @@ -2,15 +2,41 @@ { public enum ShapeType { + /// + /// Default type for SkiaShape + /// Rectangle, + Circle, + Ellipse, + Arc, + /// - /// TODO + /// TODO unimplemented yet /// Squricle, + /// + /// + /// + /// Path, + + /// + /// Uses multiple Points + /// + /// + Polygon, + + /// + /// Uses multiple Points + /// + Line, + + /// + /// unimplemented + /// Custom } -} \ No newline at end of file +} diff --git a/src/Engine/Internals/Extensions/InternalExtensions.Maui.cs b/src/Engine/Internals/Extensions/InternalExtensions.Maui.cs new file mode 100644 index 00000000..70902160 --- /dev/null +++ b/src/Engine/Internals/Extensions/InternalExtensions.Maui.cs @@ -0,0 +1,183 @@ +using Microsoft.Maui.Controls.Shapes; + +namespace DrawnUi.Maui.Extensions; + +public static partial class InternalExtensions +{ + + #region MAUI CONTEXT + + public static IMauiContext? FindMauiContext(this Element element, bool fallbackToAppMauiContext = false) + { + if (element is IElement fe && fe.Handler?.MauiContext != null) + return fe.Handler.MauiContext; + + foreach (var parent in element.GetParentsPath()) + { + if (parent is IElement parentView && parentView.Handler?.MauiContext != null) + return parentView.Handler.MauiContext; + } + + return fallbackToAppMauiContext ? Application.Current?.FindMauiContext() : default; + } + + public static IEnumerable GetParentsPath(this Element self) + { + Element current = self; + + while (!IsAppOrNull(current.RealParent)) + { + current = current.RealParent; + yield return current; + } + } + + internal static bool IsAppOrNull(object? element) => + element == null || element is IApplication; + + #endregion + + public static SkiaShadow FromPlatform(this object platform) + { + if (platform is Shadow shadow) + { + return new SkiaShadow + { + Blur = shadow.Radius, + Opacity = shadow.Opacity, + X = shadow.Offset.X, + Y = shadow.Offset.Y, + Color = ((SolidColorBrush)shadow.Brush).Color, + BindingContext = shadow.BindingContext + }; + } + return null; + } + + public static void FromPlatform(this Geometry geometry, SKPath path, SKRect destination, float scale) + { + FromPlatform(geometry, path, scale); + path.Offset(destination.Location); + } + + public static SKPath FromPlatform(this Geometry geometry, SKPath path, float scale) + { + if (geometry == null) + throw new ArgumentNullException(nameof(geometry)); + + path ??= new SKPath(); + + if (geometry is EllipseGeometry ellipseGeometry) + { + return ConvertEllipseGeometry(ellipseGeometry, path, scale); + } + else if (geometry is LineGeometry lineGeometry) + { + return ConvertLineGeometry(lineGeometry, path, scale); + } + else if (geometry is RectangleGeometry rectangleGeometry) + { + return ConvertRectangleGeometry(rectangleGeometry, path, scale); + } + else if (geometry is PathGeometry pathGeometry) + { + return ConvertPathGeometry(pathGeometry, path, scale); + } + + throw new NotSupportedException($"Unsupported geometry type: {geometry.GetType()}"); + } + + + public static SKRect ToSKRect(this Rect rect, float scale) => + new SKRect( + (float)rect.X * scale, + (float)rect.Y * scale, + (float)(rect.X + rect.Width) * scale, + (float)(rect.Y + rect.Height) * scale); + + private static SKPath ConvertEllipseGeometry(EllipseGeometry ellipseGeometry, SKPath path, float scale) + { + + var rect = new SKRect(0, 0, (float)(ellipseGeometry.RadiusX * 2 * scale), (float)(ellipseGeometry.RadiusY * 2 * scale)); + path.AddOval(rect); + return path; + } + + private static SKPoint ToSKPoint(this Point point, float scale) + { + return new SKPoint((float)(point.X * scale), (float)(point.Y * scale)); + } + + private static SKPath ConvertLineGeometry(LineGeometry lineGeometry, SKPath path, float scale) + { + + path.MoveTo(lineGeometry.StartPoint.ToSKPoint(scale)); + path.LineTo(lineGeometry.EndPoint.ToSKPoint(scale)); + return path; + } + + private static SKPath ConvertRectangleGeometry(RectangleGeometry rectangleGeometry, SKPath path, float scale) + { + + var rect = rectangleGeometry.Rect.ToSKRect(scale); + path.AddRect(rect); + return path; + } + + private static SKPath ConvertPathGeometry(PathGeometry pathGeometry, SKPath path, float scale) + { + + + foreach (var figure in pathGeometry.Figures) + { + if (figure.StartPoint != null) + path.MoveTo(figure.StartPoint.ToSKPoint(scale)); + + foreach (var segment in figure.Segments) + { + switch (segment) + { + case LineSegment lineSegment: + path.LineTo(lineSegment.Point.ToSKPoint(scale)); + break; + + case BezierSegment bezierSegment: + path.CubicTo( + bezierSegment.Point1.ToSKPoint(scale), + bezierSegment.Point2.ToSKPoint(scale), + bezierSegment.Point3.ToSKPoint(scale)); + break; + + case PolyLineSegment polyLineSegment: + foreach (var point in polyLineSegment.Points) + { + path.LineTo(point.ToSKPoint(scale)); + } + break; + + case PolyBezierSegment polyBezierSegment: + for (int i = 0; i < polyBezierSegment.Points.Count; i += 3) + { + path.CubicTo( + polyBezierSegment.Points[i].ToSKPoint(scale), + polyBezierSegment.Points[i + 1].ToSKPoint(scale), + polyBezierSegment.Points[i + 2].ToSKPoint(scale)); + } + break; + + default: + throw new NotSupportedException($"Unsupported segment type: {segment.GetType()}"); + } + } + + if (figure.IsClosed) + { + path.Close(); + } + } + + return path; + } + +} + diff --git a/src/Engine/Internals/Extensions/InternalExtensions.cs b/src/Engine/Internals/Extensions/InternalExtensions.cs index 78709163..fb69fad1 100644 --- a/src/Engine/Internals/Extensions/InternalExtensions.cs +++ b/src/Engine/Internals/Extensions/InternalExtensions.cs @@ -2,41 +2,9 @@ namespace DrawnUi.Maui.Extensions; -public static class InternalExtensions +public static partial class InternalExtensions { - #region MAUI CONTEXT - - public static IMauiContext? FindMauiContext(this Element element, bool fallbackToAppMauiContext = false) - { - if (element is IElement fe && fe.Handler?.MauiContext != null) - return fe.Handler.MauiContext; - - foreach (var parent in element.GetParentsPath()) - { - if (parent is IElement parentView && parentView.Handler?.MauiContext != null) - return parentView.Handler.MauiContext; - } - - return fallbackToAppMauiContext ? Application.Current?.FindMauiContext() : default; - } - - public static IEnumerable GetParentsPath(this Element self) - { - Element current = self; - - while (!IsAppOrNull(current.RealParent)) - { - current = current.RealParent; - yield return current; - } - } - - internal static bool IsAppOrNull(object? element) => - element == null || element is IApplication; - - #endregion - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IntersectsWith(this SKRect rect, SKRect with, SKPoint offset) { @@ -228,4 +196,4 @@ public static int Clamp(this int self, int min, int max) return self; } -} \ No newline at end of file +} diff --git a/src/Engine/Internals/Xaml/ColumnDefinitionTypeConverter.cs b/src/Engine/Internals/Xaml/ColumnDefinitionTypeConverter.cs index 11644557..f0055e3f 100644 --- a/src/Engine/Internals/Xaml/ColumnDefinitionTypeConverter.cs +++ b/src/Engine/Internals/Xaml/ColumnDefinitionTypeConverter.cs @@ -23,4 +23,4 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", obj, typeof(ColumnDefinition))); } -} \ No newline at end of file +} diff --git a/src/Engine/Internals/Xaml/SkiaPointCollectionConverter.cs b/src/Engine/Internals/Xaml/SkiaPointCollectionConverter.cs new file mode 100644 index 00000000..0b887074 --- /dev/null +++ b/src/Engine/Internals/Xaml/SkiaPointCollectionConverter.cs @@ -0,0 +1,61 @@ +using System.ComponentModel; +using System.Globalization; + +namespace DrawnUi.Maui.Infrastructure.Xaml +{ + public class SkiaPointCollectionConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + // We can convert from a string to an IList + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + // Convert the string to an IList + if (value is string strValue) + { + return ParseSkiaPointCollection(strValue); + } + + return base.ConvertFrom(context, culture, value); + } + + private IList ParseSkiaPointCollection(string str) + { + var points = new List(); + + if (string.IsNullOrWhiteSpace(str)) + return points; + + // Split the string into individual point representations + var pointStrings = str.Split(';'); + + foreach (var pointString in pointStrings) + { + var trimmedPointString = pointString.Trim(); + if (string.IsNullOrEmpty(trimmedPointString)) + continue; + + // Split each point into X and Y coordinates, allowing for ',' or ' ' as separators + var coordinates = trimmedPointString.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (coordinates.Length != 2) + throw new FormatException($"Invalid point format: '{trimmedPointString}'"); + + if (double.TryParse(coordinates[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double x) && + double.TryParse(coordinates[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double y)) + { + points.Add(new SkiaPoint(x, y)); + } + else + { + throw new FormatException($"Invalid coordinate values in point: '{trimmedPointString}'"); + } + } + + return points; + } + } +} diff --git a/src/Engine/Internals/Xaml/SkiaShadowsCollection.cs b/src/Engine/Internals/Xaml/SkiaShadowsCollection.cs new file mode 100644 index 00000000..08727f86 --- /dev/null +++ b/src/Engine/Internals/Xaml/SkiaShadowsCollection.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Collections.ObjectModel; + +namespace DrawnUi.Maui.Infrastructure.Xaml +{ + + public class SkiaShadowsCollection : ObservableCollection, IList + { + + public int Add(object value) + { + if (value is SkiaShadow skiaShadow) + { + base.Add(skiaShadow); + } + else + { + + var fromPlatform = InternalExtensions.FromPlatform(value); + if (fromPlatform != null) + { + base.Add(fromPlatform); + } + else + throw new InvalidOperationException("Invalid item type in Shadows collection"); + } + return Count - 1; + } + + + } +} diff --git a/src/Engine/Views/Canvas.cs b/src/Engine/Views/Canvas.cs index 6b270036..24fd1da2 100644 --- a/src/Engine/Views/Canvas.cs +++ b/src/Engine/Views/Canvas.cs @@ -1,5 +1,5 @@ -using Microsoft.Maui.Controls.Internals; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; +using Microsoft.Maui.Controls.Internals; using Size = Microsoft.Maui.Graphics.Size; namespace DrawnUi.Maui.Views; @@ -50,7 +50,6 @@ protected virtual void SetContent(SkiaControl view) } } - #region LAYOUT & AUTOSIZE private double _widthConstraint; @@ -367,9 +366,9 @@ protected virtual void OnGesturesAttachChanged() else if (this.Gestures == GesturesMode.Lock) TouchEffect.SetShareTouch(this, TouchHandlingStyle.Lock); - else - if (this.Gestures == GesturesMode.Share) - TouchEffect.SetShareTouch(this, TouchHandlingStyle.Share); + //else + //if (this.Gestures == GesturesMode.Share) + // TouchEffect.SetShareTouch(this, TouchHandlingStyle.Share); } } @@ -380,14 +379,21 @@ protected virtual void OnGesturesAttachChanged() bool _isPanning; - protected SkiaSvg DebugPointer = new() + + protected virtual SkiaSvg CreateDebugPointer() { - HeightRequest = 32, - UseCache = SkiaCacheType.Operations, - LockRatio = 1, - //https://freesvg.org/hand-pointer - SvgString = "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n image/svg+xml\n \n \n \n \n Openclipart\n \n \n pointer\n 2011-01-04T07:30:40\n \n https://openclipart.org/detail/103567/pointer-by-3dline\n \n \n 3Dline\n \n \n \n \n Graphical\n Hand\n Icon\n colored\n point\n \n \n \n \n \n \n \n \n \n \n" - }; + return new() + { + HeightRequest = 32, + UseCache = SkiaCacheType.Operations, + LockRatio = 1, + //https://freesvg.org/hand-pointer + SvgString = + "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n image/svg+xml\n \n \n \n \n Openclipart\n \n \n pointer\n 2011-01-04T07:30:40\n \n https://openclipart.org/detail/103567/pointer-by-3dline\n \n \n 3Dline\n \n \n \n \n Graphical\n Hand\n Icon\n colored\n point\n \n \n \n \n \n \n \n \n \n \n" + }; + } + + protected SkiaSvg DebugPointer { get; set; } public override void OnDisposing() { @@ -485,14 +491,16 @@ protected virtual void ProcessGestures(SkiaGesturesParameters args) /// public virtual void OnGestureEvent(TouchActionType type, TouchActionEventArgs args1, TouchActionResult touchAction) { - //Super.Log($"[Touch] Canvas got {args.Type}"); - /* + //Super.Log($"[Touch] Canvas got {args1.Type} {type} => {touchAction}"); + +#if ANDROID if (touchAction == TouchActionResult.Panning) { //filter micro-gestures if ((Math.Abs(args1.Distance.Delta.X) < 1 && Math.Abs(args1.Distance.Delta.Y) < 1) || (Math.Abs(args1.Distance.Velocity.X / RenderingScale) < 1 && Math.Abs(args1.Distance.Velocity.Y / RenderingScale) < 1)) { + //Super.Log($"[Touch] IGNORED"); return; } @@ -522,12 +530,19 @@ public virtual void OnGestureEvent(TouchActionType type, TouchActionEventArgs ar { _isPanning = false; } - */ +#endif + var args = SkiaGesturesParameters.Create(touchAction, args1); if (GesturesDebugColor.Alpha > 0) { + + if (DebugPointer == null) + { + DebugPointer = CreateDebugPointer(); + } + if (args.Type == TouchActionResult.Down) { _debugIsPressed = true; @@ -800,4 +815,4 @@ protected override void Draw(SkiaDrawingContext context, SKRect destination, flo } #endregion -} \ No newline at end of file +} diff --git a/src/Engine/Views/DrawnView.cs b/src/Engine/Views/DrawnView.cs index f3b5cf7e..1284f019 100644 --- a/src/Engine/Views/DrawnView.cs +++ b/src/Engine/Views/DrawnView.cs @@ -1327,11 +1327,19 @@ SkiaDrawingContext CreateContext(SKSurface surface) /// public event EventHandler WillFirstTimeDraw; + protected SKRect LastDrawnRect; + private bool OnDrawSurface(SKSurface surface, SKRect rect) { lock (LockDraw) { + if (LastDrawnRect.Size != rect.Size) + { + LastDrawnRect = rect; + NeedMeasure = true; + } + if (!OnStartRendering(surface.Canvas)) return UpdateMode == UpdateMode.Constant; @@ -1747,6 +1755,10 @@ protected virtual void Draw(SkiaDrawingContext context, SKRect destination, floa destination.Top + (float)Math.Round((Padding.Top) * scale), destination.Right - (float)Math.Round((Padding.Right) * scale), destination.Bottom - (float)Math.Round((Padding.Bottom) * scale)); + if (NeedMeasure) + { + child.NeedMeasure = true; + } child.Render(context, rectForChild, (float)scale); } } diff --git a/src/samples/Sandbox/App.xaml.cs b/src/samples/Sandbox/App.xaml.cs index 55a5d222..ded1f258 100644 --- a/src/samples/Sandbox/App.xaml.cs +++ b/src/samples/Sandbox/App.xaml.cs @@ -1,9 +1,4 @@ -using AppoMobi.Specials; -using DrawnUi.Maui; -using Microsoft.Maui.Platform; -using Sandbox.Views; -using System.Globalization; -using System.Reflection; +using System.Reflection; namespace Sandbox { @@ -19,7 +14,8 @@ public App() var mask = "MainPage"; - var xamlResources = this.GetType().Assembly.GetCustomAttributes(); + var xamlResources = this.GetType().Assembly + .GetCustomAttributes(); MainPages = xamlResources .Where(x => x.Type.Name.Contains(mask) @@ -66,6 +62,6 @@ public record MainPageVariant() public string Name { get; set; } } - + } diff --git a/src/samples/Sandbox/MainPage.xaml b/src/samples/Sandbox/MainPage.xaml index 5dc3c476..59546a66 100644 --- a/src/samples/Sandbox/MainPage.xaml +++ b/src/samples/Sandbox/MainPage.xaml @@ -8,6 +8,7 @@ xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections" xmlns:sandbox="clr-namespace:Sandbox" xmlns:views="clr-namespace:Sandbox.Views" + x:Name="ThisPage" x:DataType="sandbox:MainPageViewModel" BackgroundColor="#000000"> @@ -55,6 +56,7 @@ Source="car.png" Tag="Car" UseCache="ImageDoubleBuffered"> + @@ -135,6 +137,7 @@ x:Name="Buttons" HorizontalOptions="Center" IsVisible="True" + ItemsSource="{Binding Source={x:Reference ThisPage}, Path=ButtonsList}" Spacing="0" Split="0" Tag="Buttons" diff --git a/src/samples/Sandbox/MainPage.xaml.cs b/src/samples/Sandbox/MainPage.xaml.cs index 3bb09b98..77e97ab4 100644 --- a/src/samples/Sandbox/MainPage.xaml.cs +++ b/src/samples/Sandbox/MainPage.xaml.cs @@ -7,16 +7,17 @@ public partial class MainPage : BasePage { int count = 0; + public List ButtonsList { get; } + public MainPage() { try { + ButtonsList = App.MainPages; + InitializeComponent(); BindingContext = new MainPageViewModel(); - - Buttons.ItemsSource = App.MainPages; - } catch (Exception e) { @@ -57,4 +58,4 @@ private void TappedSelectPage(object sender, SkiaGesturesParameters skiaGestures -} \ No newline at end of file +} diff --git a/src/samples/Sandbox/MainPageCode.cs b/src/samples/Sandbox/MainPageCode.cs index 03cd91d7..6b750522 100644 --- a/src/samples/Sandbox/MainPageCode.cs +++ b/src/samples/Sandbox/MainPageCode.cs @@ -138,7 +138,7 @@ void Build() CornerRadius = 16, RotationX = 16, RotationY = 20, - RotationZ = 6, + RotationZ = -6, TranslationZ = 0, Tag="Shape", WidthRequest = 150, diff --git a/src/samples/Sandbox/MainPageCode2.cs b/src/samples/Sandbox/MainPageCode2.cs deleted file mode 100644 index c8de8448..00000000 --- a/src/samples/Sandbox/MainPageCode2.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Sandbox.Views; -using Canvas = DrawnUi.Maui.Views.Canvas; - -namespace Sandbox -{ - - - public class MainPageCode2 : BasePage, IDisposable - { - Canvas Canvas; - - public void Dispose() - { - this.Content = null; - Canvas?.Dispose(); - } - - public MainPageCode2() - { -#if DEBUG - HotReloadService.UpdateApplicationEvent += ReloadUI; -#endif - Build(); - } - - private int _reloads; - - internal class CustomShape : SkiaShape - { - public CustomShape() - { - Type = ShapeType.Circle; - } - - public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - if (args.Type == AppoMobi.Maui.Gestures.TouchActionResult.Tapped) - { - this.BackgroundColor = Colors.Blue; - return this; - } - - return base.ProcessGestures(args, apply); - } - } - - void Build() - { - Canvas?.Dispose(); - - Canvas = new Canvas() - { - Gestures = GesturesMode.Enabled, - HardwareAcceleration = HardwareAccelerationMode.Enabled, - - VerticalOptions = LayoutOptions.Start, - HorizontalOptions = LayoutOptions.Fill, - WidthRequest = 300, - HeightRequest = 300, - BackgroundColor = Colors.Gray, - - Content = new SkiaLayout() - { - VerticalOptions = LayoutOptions.Fill, - HorizontalOptions = LayoutOptions.Fill, - BackgroundColor = Colors.Bisque, - Children = new List() - { - new SkiaLayout() - { - HeightRequest = 200, - BackgroundColor = Colors.Green, - Children = new List() - { - new SkiaLayout() - { - ColumnSpacing = 0, - BackgroundColor = Colors.Green, - Margin=new Thickness(0), - Type = LayoutType.Grid, - RowDefinitions = new RowDefinitionCollection() - { - new RowDefinition(new GridLength(1,GridUnitType.Star)), - new RowDefinition(new GridLength(100,GridUnitType.Absolute)), - new RowDefinition(new GridLength(1,GridUnitType.Star)), - }, - Children = new List() - { - new CustomShape() - { - Tag="1", - HeightRequest = 50, - WidthRequest = 50, - HorizontalOptions = LayoutOptions.Center, - BackgroundColor = Colors.Red, - }.WithRow(0), - new SkiaShape() - { - Tag="2", - HorizontalOptions = LayoutOptions.Fill, - BackgroundColor = Colors.Yellow, - }.WithRow(1), - new CustomShape() - { - Tag="3", - HeightRequest = 50, - WidthRequest = 50, - HorizontalOptions = LayoutOptions.Center, - BackgroundColor = Colors.Red, - }.WithRow(2), - } - }, - - - } - } - - } - } - - - }; - - _reloads++; - - this.Content = Canvas; - } - - private void ReloadUI(Type[] obj) - { - MainThread.BeginInvokeOnMainThread(() => - { - Build(); - }); - } - - } -} diff --git a/src/samples/Sandbox/MainPageDevShape.cs b/src/samples/Sandbox/MainPageDevShape.cs new file mode 100644 index 00000000..3e0ef971 --- /dev/null +++ b/src/samples/Sandbox/MainPageDevShape.cs @@ -0,0 +1,140 @@ +using Sandbox.Views; +using Canvas = DrawnUi.Maui.Views.Canvas; + +namespace Sandbox +{ + + + public class MainPageDevShape : BasePage, IDisposable + { + Canvas Canvas; + + public void Dispose() + { + this.Content = null; + Canvas?.Dispose(); + } + + public MainPageDevShape() + { +#if DEBUG + HotReloadService.UpdateApplicationEvent += ReloadUI; +#endif + Build(); + } + + private int _reloads; + + + void Build() + { + Canvas?.Dispose(); + + Canvas = new Canvas() + { + Gestures = GesturesMode.Enabled, + HardwareAcceleration = HardwareAccelerationMode.Enabled, + + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Fill, + WidthRequest = 300, + HeightRequest = 300, + BackgroundColor = Colors.White, + + Content = new SkiaLayout() + { + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.Fill, + Children = new List() + { + new SkiaShape() + { + SmoothPoints = 0.9f, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + WidthRequest = 150, + HeightRequest = 150, + Type = ShapeType.Polygon, + StrokeColor = Colors.Black, + StrokeWidth = 3, + StrokeBlendMode = SKBlendMode.SrcIn, + StrokeCap = SKStrokeCap.Round, + Rotation = 0, + BackgroundColor = Colors.Yellow, + //Points = SkiaShape.CreateStarPoints(5, 0.5), + Shadows = new List() + { + + }, + Points = new List() + { + new (0.1f, 0.1f), + new (0.9f, 0.1f), + new (0.8f, 0.9f), + new (0.1f, 0.9f), + }, + }, + new SkiaShape() + { + IsVisible = false, + Type = ShapeType.Line, + StrokeColor = Colors.Red, + StrokeWidth = 2, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + Points = new List() + { + new (0.2f, 0.8f), + new (0.8f, 0.2f), + new (1.0f, 0.2f), + }, + }, + new SkiaShape() + { + IsVisible = false, + SmoothPoints = 0.3f, + Type = ShapeType.Line, + StrokeColor = Colors.Green, + StrokeWidth = 2, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + Points = new List() + { + new (0.2f, 0.8f), + new (0.8f, 0.2f), + new (1.0f, 0.2f), + }, + }, + new SkiaImage() + { + Source="car.png", + WidthRequest = 100, + HeightRequest = 100, + VerticalOptions = LayoutOptions.End, + Margin = 10, + Shadow = new Shadow() + { + Radius = 3, + Brush= Colors.Black, + Offset = new (2,2) + } + } + } + } + }; + + _reloads++; + + this.Content = Canvas; + } + + private void ReloadUI(Type[] obj) + { + MainThread.BeginInvokeOnMainThread(() => + { + Build(); + }); + } + + } +} diff --git a/src/samples/Sandbox/MainPageRepro.xaml b/src/samples/Sandbox/MainPageRepro.xaml deleted file mode 100644 index afc5ca8f..00000000 --- a/src/samples/Sandbox/MainPageRepro.xaml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/samples/Sandbox/MainPageTestImage.cs b/src/samples/Sandbox/MainPageTestImage.cs new file mode 100644 index 00000000..4e95c605 --- /dev/null +++ b/src/samples/Sandbox/MainPageTestImage.cs @@ -0,0 +1,98 @@ +using Sandbox.Views; +using Canvas = DrawnUi.Maui.Views.Canvas; + +namespace Sandbox +{ + + + public class MainPageTestImage : BasePage, IDisposable + { + Canvas Canvas; + + public void Dispose() + { + this.Content = null; + Canvas?.Dispose(); + } + + + public MainPageTestImage() + { +#if DEBUG + HotReloadService.UpdateApplicationEvent += ReloadUI; +#endif + Build(); + } + + private int _reloads; + + void Build() + { + Canvas?.Dispose(); + + Canvas = new Canvas() + { + Gestures = GesturesMode.Enabled, + HardwareAcceleration = HardwareAccelerationMode.Disabled, + + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.Fill, + BackgroundColor = Colors.LightGray, + + Content = new SkiaLayout() + { + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.Fill, + Children = new List() + { + new SkiaLayout() + { + Children = new List() + { + new SkiaShape() + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + WidthRequest = 150, + HeightRequest = 150, + Type = ShapeType.Polygon, + StrokeColor = Colors.Black, + StrokeWidth = 3, + BackgroundColor = Colors.Red, + Points = new List() + { + new (0.1f, 0.1f), + new (0.9f, 0.1f), + new (0.1f, 0.5f), + }, + Content = new SkiaImage() + { + Source = "cap.png", + Aspect = TransformAspect.AspectFit, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center + } + } + } + } + } + } + + + }; + + _reloads++; + + this.Content = Canvas; + } + + private void ReloadUI(Type[] obj) + { + MainThread.BeginInvokeOnMainThread(() => + { + Build(); + }); + } + + } +} diff --git a/src/samples/Sandbox/MauiProgram.cs b/src/samples/Sandbox/MauiProgram.cs index 2e13cf17..9ab9c4f2 100644 --- a/src/samples/Sandbox/MauiProgram.cs +++ b/src/samples/Sandbox/MauiProgram.cs @@ -1,7 +1,7 @@ global using DrawnUi.Maui.Draw; global using SkiaSharp; using Microsoft.Extensions.Logging; - + namespace Sandbox { public static class MauiProgram @@ -12,6 +12,14 @@ public static class MauiProgram public static string Testing = "1\r\n2\r\n3"; + public static List PolygonStar + { + get + { + return SkiaShape.CreateStarPoints(5); + } + } + public static MauiApp CreateMauiApp() { //SkiaImageManager.CacheLongevitySecs = 10; @@ -26,6 +34,7 @@ public static MauiApp CreateMauiApp() { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + fonts.AddFont("OpenSans-Semibold.ttf", "FontTextTitle"); fonts.AddFont("OpenSans-Regular.ttf", "FontText"); fonts.AddFont("NotoColorEmoji-Regular.ttf", "FontEmoji"); @@ -36,7 +45,7 @@ public static MauiApp CreateMauiApp() fonts.AddFont("Orbitron-ExtraBold.ttf", "FontGameExtraBold"); //800 }); - + builder.UseDrawnUi(new() { UseDesktopKeyboard = true, //will not work with maui shell on apple!! @@ -62,4 +71,4 @@ public static MauiApp CreateMauiApp() public static string ShadersFolder = "Shaders"; } -} \ No newline at end of file +} diff --git a/src/samples/Sandbox/Sandbox.csproj b/src/samples/Sandbox/Sandbox.csproj index c8c2f709..e936bd60 100644 --- a/src/samples/Sandbox/Sandbox.csproj +++ b/src/samples/Sandbox/Sandbox.csproj @@ -213,8 +213,8 @@ - - MainPageRepro.xaml + + MainPageShapes.xaml @@ -370,7 +370,7 @@ - + MSBuild:Compile diff --git a/src/samples/Sandbox/TestPage.xaml b/src/samples/Sandbox/TestPage.xaml index 7d7f5033..825cfeb4 100644 --- a/src/samples/Sandbox/TestPage.xaml +++ b/src/samples/Sandbox/TestPage.xaml @@ -27,9 +27,8 @@ HorizontalOptions="Fill" Tag="Wrapper" VerticalOptions="Fill"> - + + UseCache="None" + VerticalOptions="Center"> - + diff --git a/src/samples/Sandbox/TestPage2.xaml b/src/samples/Sandbox/TestPage2.xaml index 6a480eb8..b43cb54f 100644 --- a/src/samples/Sandbox/TestPage2.xaml +++ b/src/samples/Sandbox/TestPage2.xaml @@ -11,20 +11,77 @@ x:DataType="sandbox:MainPageViewModel" BackgroundColor="#000000"> + + - - + + - - - - + - + + + + + + + White + Yellow + Orange + Red + DarkRed + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Sandbox/Views/MainDrawnCells.xaml b/src/samples/Sandbox/Views/MainDrawnCells.xaml new file mode 100644 index 00000000..aec4d5fe --- /dev/null +++ b/src/samples/Sandbox/Views/MainDrawnCells.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Sandbox/Views/MainDrawnCells.xaml.cs b/src/samples/Sandbox/Views/MainDrawnCells.xaml.cs new file mode 100644 index 00000000..6deec270 --- /dev/null +++ b/src/samples/Sandbox/Views/MainDrawnCells.xaml.cs @@ -0,0 +1,69 @@ +namespace Sandbox.Views +{ + public partial class MainDrawnCells + { + private double _Position; + private readonly MockChat2ViewModel _vm; + + public double Position + { + get + { + return _Position; + } + set + { + if (_Position != value) + { + _Position = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowPosition)); + } + } + } + + public string ShowPosition + { + get + { + return $"{Position:0.0}"; + } + } + + public MainDrawnCells() + { + try + { + InitializeComponent(); + + BindingContext = _vm = new MockChat2ViewModel(); + + } + catch (Exception e) + { + Super.DisplayException(this, e); + Console.WriteLine(e); + } + } + + + bool once; + + protected override void OnAppearing() + { + base.OnAppearing(); + + if (!once) + { + once = true; + _vm.LoadData(); + } + + } + + private void OnScrolled(object sender, ScaledPoint e) + { + + } + } +} diff --git a/src/samples/Sandbox/Views/MainPageBackdrop.xaml b/src/samples/Sandbox/Views/MainPageBackdrop.xaml index 13829b48..7d2775c3 100644 --- a/src/samples/Sandbox/Views/MainPageBackdrop.xaml +++ b/src/samples/Sandbox/Views/MainPageBackdrop.xaml @@ -125,10 +125,10 @@ @@ -150,6 +150,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #777777 + Gray + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pin + + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Sandbox/MainPageRepro.xaml.cs b/src/samples/Sandbox/Views/MainPageShapes.xaml.cs similarity index 96% rename from src/samples/Sandbox/MainPageRepro.xaml.cs rename to src/samples/Sandbox/Views/MainPageShapes.xaml.cs index bcd0b444..b9ce1e32 100644 --- a/src/samples/Sandbox/MainPageRepro.xaml.cs +++ b/src/samples/Sandbox/Views/MainPageShapes.xaml.cs @@ -3,11 +3,11 @@ namespace MauiNet8; -public partial class MainPageRepro : BasePage +public partial class MainPageShapes : BasePage { int count = 0; - public MainPageRepro() + public MainPageShapes() { try {