diff --git a/Maui.Core/Animation/AnimatableKey.cs b/Maui.Core/Animation/AnimatableKey.cs new file mode 100644 index 000000000000..e32361fb0a8a --- /dev/null +++ b/Maui.Core/Animation/AnimatableKey.cs @@ -0,0 +1,82 @@ +using System; + +namespace System.Maui +{ + internal class AnimatableKey + { + public AnimatableKey(IAnimatable animatable, string handle) + { + if (animatable == null) + { + throw new ArgumentNullException(nameof(animatable)); + } + + if (string.IsNullOrEmpty(handle)) + { + throw new ArgumentException("Argument is null or empty", nameof(handle)); + } + + Animatable = new WeakReference(animatable); + Handle = handle; + } + + public WeakReference Animatable { get; } + + public string Handle { get; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + if (ReferenceEquals(this, obj)) + { + return true; + } + if (obj.GetType() != GetType()) + { + return false; + } + return Equals((AnimatableKey)obj); + } + + public override int GetHashCode() + { + unchecked + { + IAnimatable target; + if (!Animatable.TryGetTarget(out target)) + { + return Handle?.GetHashCode() ?? 0; + } + + return ((target?.GetHashCode() ?? 0) * 397) ^ (Handle?.GetHashCode() ?? 0); + } + } + + protected bool Equals(AnimatableKey other) + { + if (!string.Equals(Handle, other.Handle)) + { + return false; + } + + IAnimatable thisAnimatable; + + if (!Animatable.TryGetTarget(out thisAnimatable)) + { + return false; + } + + IAnimatable thatAnimatable; + + if (!other.Animatable.TryGetTarget(out thatAnimatable)) + { + return false; + } + + return Equals(thisAnimatable, thatAnimatable); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Animation/Animation.cs b/Maui.Core/Animation/Animation.cs new file mode 100644 index 000000000000..75daa7ded44b --- /dev/null +++ b/Maui.Core/Animation/Animation.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Collections.Generic; + +namespace System.Maui +{ + public class Animation : IEnumerable + { + readonly List _children; + readonly Easing _easing; + readonly Action _finished; + readonly Action _step; + double _beginAt; + double _finishAt; + bool _finishedTriggered; + + public Animation() + { + _children = new List(); + _easing = Easing.Linear; + _step = f => { }; + } + + public Animation(Action callback, double start = 0.0f, double end = 1.0f, Easing easing = null, Action finished = null) + { + _children = new List(); + _easing = easing ?? Easing.Linear; + _finished = finished; + + Func transform = AnimationExtensions.Interpolate(start, end); + _step = f => callback(transform(f)); + } + + public IEnumerator GetEnumerator() + { + return _children.GetEnumerator(); + } + + public void Add(double beginAt, double finishAt, Animation animation) + { + if (beginAt < 0 || beginAt > 1) + throw new ArgumentOutOfRangeException("beginAt"); + + if (finishAt < 0 || finishAt > 1) + throw new ArgumentOutOfRangeException("finishAt"); + + if (finishAt <= beginAt) + throw new ArgumentException("finishAt must be greater than beginAt"); + + animation._beginAt = beginAt; + animation._finishAt = finishAt; + _children.Add(animation); + } + + public void Commit(IAnimatable owner, string name, uint rate = 16, uint length = 250, Easing easing = null, Action finished = null, Func repeat = null) + { + owner.Animate(name, this, rate, length, easing, finished, repeat); + } + + public Action GetCallback() + { + Action result = f => + { + _step(_easing.Ease(f)); + foreach (Animation animation in _children) + { + if (animation._finishedTriggered) + continue; + + double val = Math.Max(0.0f, Math.Min(1.0f, (f - animation._beginAt) / (animation._finishAt - animation._beginAt))); + + if (val <= 0.0f) // not ready to process yet + continue; + + Action callback = animation.GetCallback(); + callback(val); + + if (val >= 1.0f) + { + animation._finishedTriggered = true; + if (animation._finished != null) + animation._finished(); + } + } + }; + return result; + } + + internal void ResetChildren() + { + foreach (var anim in _children) + anim._finishedTriggered = false; + } + + public Animation Insert(double beginAt, double finishAt, Animation animation) + { + Add(beginAt, finishAt, animation); + return this; + } + + public Animation WithConcurrent(Animation animation, double beginAt = 0.0f, double finishAt = 1.0f) + { + animation._beginAt = beginAt; + animation._finishAt = finishAt; + _children.Add(animation); + return this; + } + + public Animation WithConcurrent(Action callback, double start = 0.0f, double end = 1.0f, Easing easing = null, double beginAt = 0.0f, double finishAt = 1.0f) + { + var child = new Animation(callback, start, end, easing); + child._beginAt = beginAt; + child._finishAt = finishAt; + _children.Add(child); + return this; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Animation/AnimationExtensions.cs b/Maui.Core/Animation/AnimationExtensions.cs new file mode 100644 index 000000000000..5114c2bb8907 --- /dev/null +++ b/Maui.Core/Animation/AnimationExtensions.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Maui.Internals; + +namespace System.Maui +{ + public static class AnimationExtensions + { + static readonly Dictionary s_animations; + static readonly Dictionary s_kinetics; + + static AnimationExtensions() + { + s_animations = new Dictionary(); + s_kinetics = new Dictionary(); + } + + public static bool AbortAnimation(this IAnimatable self, string handle) + { + var key = new AnimatableKey(self, handle); + + if (!s_animations.ContainsKey(key) && !s_kinetics.ContainsKey(key)) + { + return false; + } + + Action abort = () => + { + AbortAnimation(key); + AbortKinetic(key); + }; + + DoAction(self, abort); + + return true; + } + + public static void Animate(this IAnimatable self, string name, Animation animation, uint rate = 16, uint length = 250, Easing easing = null, Action finished = null, + Func repeat = null) + { + if (repeat == null) + self.Animate(name, animation.GetCallback(), rate, length, easing, finished, null); + else { + Func r = () => + { + var val = repeat(); + if (val) + animation.ResetChildren(); + return val; + }; + self.Animate(name, animation.GetCallback(), rate, length, easing, finished, r); + } + } + + public static void Animate(this IAnimatable self, string name, Action callback, double start, double end, uint rate = 16, uint length = 250, Easing easing = null, + Action finished = null, Func repeat = null) + { + self.Animate(name, Interpolate(start, end), callback, rate, length, easing, finished, repeat); + } + + public static void Animate(this IAnimatable self, string name, Action callback, uint rate = 16, uint length = 250, Easing easing = null, Action finished = null, + Func repeat = null) + { + self.Animate(name, x => x, callback, rate, length, easing, finished, repeat); + } + + public static void Animate(this IAnimatable self, string name, Func transform, Action callback, + uint rate = 16, uint length = 250, Easing easing = null, + Action finished = null, Func repeat = null) + { + if (transform == null) + throw new ArgumentNullException(nameof(transform)); + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + if (self == null) + throw new ArgumentNullException(nameof(self)); + + Action animate = () => AnimateInternal(self, name, transform, callback, rate, length, easing, finished, repeat); + DoAction(self, animate); + } + + + public static void AnimateKinetic(this IAnimatable self, string name, Func callback, double velocity, double drag, Action finished = null) + { + Action animate = () => AnimateKineticInternal(self, name, callback, velocity, drag, finished); + DoAction(self, animate); + } + + public static bool AnimationIsRunning(this IAnimatable self, string handle) + { + var key = new AnimatableKey(self, handle); + return s_animations.ContainsKey(key); + } + + public static Func Interpolate(double start, double end = 1.0f, double reverseVal = 0.0f, bool reverse = false) + { + double target = reverse ? reverseVal : end; + return x => start + (target - start) * x; + } + + public static IDisposable Batch(this IAnimatable self) => new BatchObject(self); + + static void AbortAnimation(AnimatableKey key) + { + // If multiple animations on the same view with the same name (IOW, the same AnimatableKey) are invoked + // asynchronously (e.g., from the `[Animate]To` methods in `ViewExtensions`), it's possible to get into + // a situation where after invoking the `Finished` handler below `s_animations` will have a new `Info` + // object in it with the same AnimatableKey. We need to continue cancelling animations until that is no + // longer the case; thus, the `while` loop. + + // If we don't cancel all of the animations popping in with this key, `AnimateInternal` will overwrite one + // of them with the new `Info` object, and the overwritten animation will never complete; any `await` for + // it will never return. + + while (s_animations.ContainsKey(key)) + { + Info info = s_animations[key]; + + s_animations.Remove(key); + + info.Tweener.ValueUpdated -= HandleTweenerUpdated; + info.Tweener.Finished -= HandleTweenerFinished; + info.Tweener.Stop(); + info.Finished?.Invoke(1.0f, true); + } + } + + static void AbortKinetic(AnimatableKey key) + { + if (!s_kinetics.ContainsKey(key)) + { + return; + } + + Ticker.Default.Remove(s_kinetics[key]); + s_kinetics.Remove(key); + } + + static void AnimateInternal(IAnimatable self, string name, Func transform, Action callback, + uint rate, uint length, Easing easing, Action finished, Func repeat) + { + var key = new AnimatableKey(self, name); + + AbortAnimation(key); + + Action step = f => callback(transform(f)); + Action final = null; + if (finished != null) + final = (f, b) => finished(transform(f), b); + + var info = new Info { Rate = rate, Length = length, Easing = easing ?? Easing.Linear }; + + var tweener = new Tweener(info.Length); + tweener.Handle = key; + tweener.ValueUpdated += HandleTweenerUpdated; + tweener.Finished += HandleTweenerFinished; + + info.Tweener = tweener; + info.Callback = step; + info.Finished = final; + info.Repeat = repeat; + info.Owner = new WeakReference(self); + + s_animations[key] = info; + + info.Callback(0.0f); + tweener.Start(); + } + + static void AnimateKineticInternal(IAnimatable self, string name, Func callback, double velocity, double drag, Action finished = null) + { + var key = new AnimatableKey(self, name); + + AbortKinetic(key); + + double sign = velocity / Math.Abs(velocity); + velocity = Math.Abs(velocity); + + int tick = Ticker.Default.Insert(step => { + long ms = step; + + velocity -= drag * ms; + velocity = Math.Max(0, velocity); + + var result = false; + if (velocity > 0) + { + result = callback(sign * velocity * ms, velocity); + } + + if (!result) + { + finished?.Invoke(); + s_kinetics.Remove(key); + } + return result; + }); + + s_kinetics[key] = tick; + } + + static void HandleTweenerFinished(object o, EventArgs args) + { + var tweener = o as Tweener; + Info info; + if (tweener != null && s_animations.TryGetValue(tweener.Handle, out info)) + { + IAnimatable owner; + if (info.Owner.TryGetTarget(out owner)) + owner.BatchBegin(); + info.Callback(tweener.Value); + + var repeat = false; + + // If the Ticker has been disabled (e.g., by power save mode), then don't repeat the animation + if (info.Repeat != null && Ticker.Default.SystemEnabled) + repeat = info.Repeat(); + + if (!repeat) + { + s_animations.Remove(tweener.Handle); + tweener.ValueUpdated -= HandleTweenerUpdated; + tweener.Finished -= HandleTweenerFinished; + } + + info.Finished?.Invoke(tweener.Value, false); + + if (info.Owner.TryGetTarget(out owner)) + owner.BatchCommit(); + + if (repeat) + { + tweener.Start(); + } + } + } + + static void HandleTweenerUpdated(object o, EventArgs args) + { + var tweener = o as Tweener; + Info info; + IAnimatable owner; + + if (tweener != null && s_animations.TryGetValue(tweener.Handle, out info) && info.Owner.TryGetTarget(out owner)) + { + owner.BatchBegin(); + info.Callback(info.Easing.Ease(tweener.Value)); + owner.BatchCommit(); + } + } + + static void DoAction(IAnimatable self, Action action) + { + //TODO: Fix this! + //if (self is BindableObject element) + //{ + // if (element.Dispatcher.IsInvokeRequired) + // { + // element.Dispatcher.BeginInvokeOnMainThread(action); + // } + // else + // { + // action(); + // } + + // return; + //} + + //if (Device.IsInvokeRequired) + //{ + // Device.BeginInvokeOnMainThread(action); + //} + //else + //{ + action(); + //} + } + + class Info + { + public Action Callback; + public Action Finished; + public Func Repeat; + public Tweener Tweener; + + public Easing Easing { get; set; } + + public uint Length { get; set; } + + public WeakReference Owner { get; set; } + + public uint Rate { get; set; } + } + + sealed class BatchObject : IDisposable + { + IAnimatable _animatable; + + public BatchObject(IAnimatable animatable) + { + _animatable = animatable; + _animatable?.BatchBegin(); + } + + public void Dispose() + { + _animatable?.BatchCommit(); + _animatable = null; + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Animation/Easing.cs b/Maui.Core/Animation/Easing.cs new file mode 100644 index 000000000000..b21fa13ac608 --- /dev/null +++ b/Maui.Core/Animation/Easing.cs @@ -0,0 +1,70 @@ +namespace System.Maui +{ + public class Easing + { + public static readonly Easing Linear = new Easing(x => x); + + public static readonly Easing SinOut = new Easing(x => Math.Sin(x * Math.PI * 0.5f)); + public static readonly Easing SinIn = new Easing(x => 1.0f - Math.Cos(x * Math.PI * 0.5f)); + public static readonly Easing SinInOut = new Easing(x => -Math.Cos(Math.PI * x) / 2.0f + 0.5f); + + public static readonly Easing CubicIn = new Easing(x => x * x * x); + public static readonly Easing CubicOut = new Easing(x => Math.Pow(x - 1.0f, 3.0f) + 1.0f); + + public static readonly Easing CubicInOut = new Easing(x => x < 0.5f ? Math.Pow(x * 2.0f, 3.0f) / 2.0f : (Math.Pow((x - 1) * 2.0f, 3.0f) + 2.0f) / 2.0f); + + public static readonly Easing BounceOut; + public static readonly Easing BounceIn; + + public static readonly Easing SpringIn = new Easing(x => x * x * ((1.70158f + 1) * x - 1.70158f)); + public static readonly Easing SpringOut = new Easing(x => (x - 1) * (x - 1) * ((1.70158f + 1) * (x - 1) + 1.70158f) + 1); + + readonly Func _easingFunc; + + static Easing() + { + BounceOut = new Easing(p => + { + if (p < 1 / 2.75f) + { + return 7.5625f * p * p; + } + if (p < 2 / 2.75f) + { + p -= 1.5f / 2.75f; + + return 7.5625f * p * p + .75f; + } + if (p < 2.5f / 2.75f) + { + p -= 2.25f / 2.75f; + + return 7.5625f * p * p + .9375f; + } + p -= 2.625f / 2.75f; + + return 7.5625f * p * p + .984375f; + }); + + BounceIn = new Easing(p => 1.0f - BounceOut.Ease(1 - p)); + } + + public Easing(Func easingFunc) + { + if (easingFunc == null) + throw new ArgumentNullException("easingFunc"); + + _easingFunc = easingFunc; + } + + public double Ease(double v) + { + return _easingFunc(v); + } + + public static implicit operator Easing(Func func) + { + return new Easing(func); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Animation/IAnimatable.cs b/Maui.Core/Animation/IAnimatable.cs new file mode 100644 index 000000000000..8474445fab6b --- /dev/null +++ b/Maui.Core/Animation/IAnimatable.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface IAnimatable + { + void BatchBegin(); + void BatchCommit(); + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/ContainerView.Android.cs b/Maui.Core/Controls/ContainerView.Android.cs new file mode 100644 index 000000000000..0a5d3a9dbba5 --- /dev/null +++ b/Maui.Core/Controls/ContainerView.Android.cs @@ -0,0 +1,68 @@ +using Android.Content; +using Android.Views; +using Android.Widget; +using Android.Graphics; +using APath = Android.Graphics.Path; + +namespace System.Maui.Core.Controls { + public partial class ContainerView : FrameLayout + { + + public ContainerView(Context context) : base(context) + { + //this.SetWillNotDraw(false); + //this.SetLayerType(LayerType.Hardware,null); + this.LayoutParameters = new LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent); + } + + View _mainView; + public View MainView { + get => _mainView; + set + { + if (_mainView == value) + return; + if (_mainView != null) + { + RemoveView(_mainView); + } + + _mainView = value; + var parent = _mainView?.Parent as ViewGroup; + var index = parent?.IndexOfChild(_mainView); + if (_mainView != null) + { + _mainView.LayoutParameters = new ViewGroup.LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent); + AddView(_mainView); + //parent?.AddView(this, index.Value); + + } + } + } + + + APath currentPath; + Size lastPathSize; + protected override void DispatchDraw(Canvas canvas) + { + if (ClipShape != null) + { + var bounds = new Rectangle(0, 0, canvas.Width, canvas.Height); + if (lastPathSize != bounds.Size || currentPath == null) + { + var path = ClipShape.PathForBounds(bounds); + currentPath = path.AsAndroidPath(); + lastPathSize = bounds.Size; + } + canvas.ClipPath(currentPath); + } + base.DispatchDraw(canvas); + } + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + _mainView?.Measure(widthMeasureSpec, heightMeasureSpec); + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + this.SetMeasuredDimension(_mainView.MeasuredWidth, _mainView.MeasuredHeight); + } + } +} diff --git a/Maui.Core/Controls/ContainerView.Mac.cs b/Maui.Core/Controls/ContainerView.Mac.cs new file mode 100644 index 000000000000..a82cba88d447 --- /dev/null +++ b/Maui.Core/Controls/ContainerView.Mac.cs @@ -0,0 +1,7 @@ +using System; +using AppKit; +namespace System.Maui.Core.Controls { + public partial class ContainerView : NSView{ + public NSView MainView { get; set; } + } +} diff --git a/Maui.Core/Controls/ContainerView.Win32.cs b/Maui.Core/Controls/ContainerView.Win32.cs new file mode 100644 index 000000000000..0958506d8756 --- /dev/null +++ b/Maui.Core/Controls/ContainerView.Win32.cs @@ -0,0 +1,9 @@ +using System.Windows.Controls; + +namespace System.Maui.Core.Controls +{ + public partial class ContainerView : UserControl + { + public UserControl MainView { get; set; } + } +} diff --git a/Maui.Core/Controls/ContainerView.cs b/Maui.Core/Controls/ContainerView.cs new file mode 100644 index 000000000000..3ee3f156898b --- /dev/null +++ b/Maui.Core/Controls/ContainerView.cs @@ -0,0 +1,21 @@ +using System; +using System.Maui.Shapes; + +namespace System.Maui.Core.Controls { + public partial class ContainerView + { + private IShape _clipShape; + public IShape ClipShape { + get => _clipShape; + set + { + if (_clipShape == value) + return; + _clipShape = value; + ClipShapeChanged(); + } + } + + partial void ClipShapeChanged(); + } +} diff --git a/Maui.Core/Controls/ContainerView.iOS.cs b/Maui.Core/Controls/ContainerView.iOS.cs new file mode 100644 index 000000000000..a4f15f293896 --- /dev/null +++ b/Maui.Core/Controls/ContainerView.iOS.cs @@ -0,0 +1,90 @@ +using CoreAnimation; +using CoreGraphics; +using UIKit; + +namespace System.Maui.Core.Controls +{ + public partial class ContainerView : UIView + { + public ContainerView() + { + AutosizesSubviews = true; + } + + UIView _mainView; + public UIView MainView + { + get => _mainView; + set + { + if (_mainView == value) + return; + + if (_mainView != null) + { + //Cleanup! + _mainView.RemoveFromSuperview(); + } + _mainView = value; + if (value == null) + return; + this.Frame = _mainView.Frame; + var oldParent = value.Superview; + if (oldParent != null) + oldParent.InsertSubviewAbove(this, _mainView); + + + //_size = _mainView.Bounds.Size; + _mainView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; + _mainView.Frame = Bounds; + + //if (_overlayView != null) + // InsertSubviewBelow (_mainView, _overlayView); + //else + AddSubview(_mainView); + } + } + + public override void SizeToFit() + { + MainView.SizeToFit(); + this.Bounds = MainView.Bounds; + base.SizeToFit(); + } + + CAShapeLayer Mask + { + get => MainView.Layer.Mask as CAShapeLayer; + set => MainView.Layer.Mask = value; + } + + partial void ClipShapeChanged() + { + lastMaskSize = Size.Zero; + if (Frame == CGRect.Empty) + return; + } + public override void LayoutSubviews() + { + base.LayoutSubviews(); + SetClipShape(); + } + + Size lastMaskSize = Size.Zero; + void SetClipShape() + { + var mask = Mask; + if (mask == null && ClipShape == null) + return; + mask ??= Mask = new CAShapeLayer(); + var frame = Frame; + var bounds = new Rectangle(0, 0, frame.Width, frame.Height); + if (bounds.Size == lastMaskSize) + return; + lastMaskSize = bounds.Size; + var path = _clipShape?.PathForBounds(bounds); + mask.Path = path?.ToCGPath(); + } + + } +} diff --git a/Maui.Core/Controls/MauiButton.Win32.cs b/Maui.Core/Controls/MauiButton.Win32.cs new file mode 100644 index 000000000000..c336a668158e --- /dev/null +++ b/Maui.Core/Controls/MauiButton.Win32.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Maui.Platform; +using System.Windows; +using System.Windows.Controls; +using WButton = System.Windows.Controls.Button; + +namespace System.Maui.Core.Controls +{ + public class MauiButton : WButton + { + public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(int), typeof(MauiButton), + new PropertyMetadata(default(int), OnCornerRadiusChanged)); + + Border _contentPresenter; + + + public int CornerRadius + { + get + { + return (int)GetValue(CornerRadiusProperty); + } + set + { + SetValue(CornerRadiusProperty, value); + } + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _contentPresenter = this.GetChildren().FirstOrDefault(); + UpdateCornerRadius(); + } + + static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((MauiButton)d).UpdateCornerRadius(); + } + + void UpdateCornerRadius() + { + if (_contentPresenter != null) + _contentPresenter.CornerRadius = new System.Windows.CornerRadius(CornerRadius); + } + } +} diff --git a/Maui.Core/Controls/MauiCheckBox.Win32.cs b/Maui.Core/Controls/MauiCheckBox.Win32.cs new file mode 100644 index 000000000000..ab7a78eb029a --- /dev/null +++ b/Maui.Core/Controls/MauiCheckBox.Win32.cs @@ -0,0 +1,41 @@ +using WCheckBox = System.Windows.Controls.CheckBox; +using WControl = System.Windows.Controls.Control; +using System.Windows; +using System.Windows.Media; +using System.Maui.Platform; + +namespace System.Maui.Core.Controls +{ + public class MauiCheckBox : WCheckBox + { + + public static readonly DependencyProperty TintBrushProperty = + DependencyProperty.Register(nameof(TintBrush), typeof(Brush), typeof(MauiCheckBox), + new PropertyMetadata(default(Brush), OnTintBrushPropertyChanged)); + + static void OnTintBrushPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var checkBox = (MauiCheckBox)d; + + if (e.NewValue is SolidColorBrush solidBrush && solidBrush.Color.A == 0) + { + checkBox.BorderBrush = Color.Black.ToBrush(); + } + else if (e.NewValue is SolidColorBrush b) + { + checkBox.BorderBrush = b; + } + } + + public MauiCheckBox() + { + BorderBrush = Color.Black.ToBrush(); + } + + public Brush TintBrush + { + get { return (Brush)GetValue(TintBrushProperty); } + set { SetValue(TintBrushProperty, value); } + } + } +} diff --git a/Maui.Core/Controls/MauiEditor.Android.cs b/Maui.Core/Controls/MauiEditor.Android.cs new file mode 100644 index 000000000000..f02c72668ead --- /dev/null +++ b/Maui.Core/Controls/MauiEditor.Android.cs @@ -0,0 +1,13 @@ +using Android.Content; +using Android.Widget; + +namespace System.Maui.Core.Controls +{ + public class MauiEditor : EditText + { + public MauiEditor(Context context) : base(context) + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/MauiEditor.iOS.cs b/Maui.Core/Controls/MauiEditor.iOS.cs new file mode 100644 index 000000000000..b19e602f6f2c --- /dev/null +++ b/Maui.Core/Controls/MauiEditor.iOS.cs @@ -0,0 +1,30 @@ +using CoreGraphics; +using UIKit; + +namespace System.Maui.Core.Controls +{ + internal interface IMauiEditor + { + event EventHandler FrameChanged; + } + + public class MauiEditor : UITextView, IMauiEditor + { + public event EventHandler FrameChanged; + + public MauiEditor(CGRect frame) : base(frame) + { + + } + + public override CGRect Frame + { + get => base.Frame; + set + { + base.Frame = value; + FrameChanged?.Invoke(this, EventArgs.Empty); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/MauiPicker.Android.cs b/Maui.Core/Controls/MauiPicker.Android.cs new file mode 100644 index 000000000000..4cbc4ded1510 --- /dev/null +++ b/Maui.Core/Controls/MauiPicker.Android.cs @@ -0,0 +1,50 @@ +using Android.Content; +using Android.Views; +using Android.Widget; +using Android.Graphics; +using Android.Runtime; +using System.Maui.Platform; +#if __ANDROID_29__ +using AndroidX.Core.Graphics.Drawable; +#else +using Android.Support.V4.Graphics.Drawable; +#endif + +namespace System.Maui.Core.Controls +{ + public class MauiPicker : MauiPickerBase + { + public MauiPicker(Context context) : base(context) + { + PickerManager.Init(this); + } + + public override bool OnTouchEvent(MotionEvent e) + { + PickerManager.OnTouchEvent(this, e); + return base.OnTouchEvent(e); // Raises the OnClick event if focus is already received + } + + protected override void OnFocusChanged(bool gainFocus, [GeneratedEnum] FocusSearchDirection direction, Rect previouslyFocusedRect) + { + base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect); + PickerManager.OnFocusChanged(gainFocus, this); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + PickerManager.Dispose(this); + + base.Dispose(disposing); + } + } + + public class MauiPickerBase : EditText + { + public MauiPickerBase(Context context) : base(context) + { + DrawableCompat.Wrap(Background); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/MauiPicker.iOS.cs b/Maui.Core/Controls/MauiPicker.iOS.cs new file mode 100644 index 000000000000..7f1203cad9b9 --- /dev/null +++ b/Maui.Core/Controls/MauiPicker.iOS.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Foundation; +using UIKit; +using ObjCRuntime; +using RectangleF = CoreGraphics.CGRect; + +namespace System.Maui.Core.Controls +{ + public class MauiPicker : NoCaretField + { + readonly HashSet _enableActions; + + public MauiPicker() + { + string[] actions = { "copy:", "select:", "selectAll:" }; + _enableActions = new HashSet(actions); + } + + public override bool CanPerform(Selector action, NSObject withSender) + => _enableActions.Contains(action.Name); + } + + public class NoCaretField : UITextField + { + public NoCaretField() : base(new RectangleF()) + { + SpellCheckingType = UITextSpellCheckingType.No; + AutocorrectionType = UITextAutocorrectionType.No; + AutocapitalizationType = UITextAutocapitalizationType.None; + } + + public override RectangleF GetCaretRectForPosition(UITextPosition position) + { + return new RectangleF(); + } + } +} diff --git a/Maui.Core/Controls/MauiProgressRing.xaml b/Maui.Core/Controls/MauiProgressRing.xaml new file mode 100644 index 000000000000..093e0f110b1e --- /dev/null +++ b/Maui.Core/Controls/MauiProgressRing.xaml @@ -0,0 +1,137 @@ + + + + + + + + Visible + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Maui.Core/Controls/MauiProgressRing.xaml.cs b/Maui.Core/Controls/MauiProgressRing.xaml.cs new file mode 100644 index 000000000000..380771636546 --- /dev/null +++ b/Maui.Core/Controls/MauiProgressRing.xaml.cs @@ -0,0 +1,59 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Animation; + +namespace System.Maui.Core.Controls +{ + public partial class MauiProgressRing : UserControl + { + public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("IsActive", typeof(bool), typeof(MauiProgressRing), new PropertyMetadata(false, new PropertyChangedCallback(IsActiveChanged))); + + Storyboard animation; + + public MauiProgressRing() + { + InitializeComponent(); + + animation = (Storyboard)Resources["ProgressRingStoryboard"]; + } + + public bool IsActive + { + get + { + return (bool)GetValue(IsActiveProperty); + } + + set + { + SetValue(IsActiveProperty, value); + } + } + + static void IsActiveChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + ((MauiProgressRing)sender).OnIsActiveChanged(Convert.ToBoolean(e.NewValue)); + } + + void OnIsActiveChanged(bool newValue) + { + if (newValue) + { + animation.Begin(); + } + else + { + animation.Stop(); + } + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + // force the ring to the largest square which is fully visible in the control + Ring.Width = Math.Min(ActualWidth, ActualHeight); + Ring.Height = Math.Min(ActualWidth, ActualHeight); + base.OnRenderSizeChanged(sizeInfo); + } + } +} diff --git a/Maui.Core/Controls/MauiSlider.Android.cs b/Maui.Core/Controls/MauiSlider.Android.cs new file mode 100644 index 000000000000..338de5df9668 --- /dev/null +++ b/Maui.Core/Controls/MauiSlider.Android.cs @@ -0,0 +1,48 @@ +using Android.Widget; +using Android.Content; +using Android.Views; + +namespace System.Maui.Core.Controls +{ + public class MauiSlider : SeekBar + { + bool _isTouching = false; + + public MauiSlider(Context context) : base(context) + { + // This should work, but it doesn't. + DuplicateParentStateEnabled = false; + } + + public override bool OnTouchEvent(MotionEvent e) + { + switch (e.Action) + { + case MotionEventActions.Down: + _isTouching = true; + break; + case MotionEventActions.Up: + Pressed = false; + break; + } + + return base.OnTouchEvent(e); + } + + public override bool Pressed + { + get + { + return base.Pressed; + } + set + { + if (_isTouching) + { + base.Pressed = value; + _isTouching = value; + } + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/MauiSlider.Mac.cs b/Maui.Core/Controls/MauiSlider.Mac.cs new file mode 100644 index 000000000000..222fff88c8ce --- /dev/null +++ b/Maui.Core/Controls/MauiSlider.Mac.cs @@ -0,0 +1,24 @@ +using AppKit; +using CoreGraphics; + +namespace System.Maui.Core.Controls +{ + public class MauiSlider : NSSlider + { + readonly CGSize _fitSize; + + internal MauiSlider() : base(CGRect.Empty) + { + Continuous = true; + SizeToFit(); + + var size = Bounds.Size; + + // This size will be set as default for horizontal NSSlider, if you try to create it via XCode (drag and drope) + // See this screenshot: https://user-images.githubusercontent.com/10124814/52661252-aecb8100-2f12-11e9-8f45-c0dab8bc8ffc.png + _fitSize = size.Width > 0 && size.Height > 0 ? size : new CGSize(96, 21); + } + + public override CGSize SizeThatFits(CGSize size) => _fitSize; + } +} \ No newline at end of file diff --git a/Maui.Core/Controls/MauiTextBox.Win32.cs b/Maui.Core/Controls/MauiTextBox.Win32.cs new file mode 100644 index 000000000000..ba66c8b3a2bc --- /dev/null +++ b/Maui.Core/Controls/MauiTextBox.Win32.cs @@ -0,0 +1,298 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Controls; +using System.Windows.Threading; + +namespace System.Maui.Core.Controls +{ + public class MauiTextBox : TextBox + { + const char ObfuscationCharacter = '●'; + + public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register("PlaceholderText", typeof(string), typeof(MauiTextBox), + new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty PlaceholderForegroundBrushProperty = DependencyProperty.Register("PlaceholderForegroundBrush", typeof(Brush), typeof(MauiTextBox), + new PropertyMetadata(default(Brush))); + + public static readonly DependencyProperty IsPasswordProperty = DependencyProperty.Register("IsPassword", typeof(bool), typeof(MauiTextBox), + new PropertyMetadata(default(bool), OnIsPasswordChanged)); + + public new static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MauiTextBox), + new PropertyMetadata("", TextPropertyChanged)); + + protected internal static readonly DependencyProperty DisabledTextProperty = DependencyProperty.Register("DisabledText", typeof(string), typeof(MauiTextBox), + new PropertyMetadata("")); + + static InputScope s_passwordInputScope; + InputScope _cachedInputScope; + CancellationTokenSource _cts; + bool _internalChangeFlag; + int _cachedSelectionLength; + + public MauiTextBox() + { + TextChanged += OnTextChanged; + SelectionChanged += OnSelectionChanged; + } + + public bool IsPassword + { + get { return (bool)GetValue(IsPasswordProperty); } + set { SetValue(IsPasswordProperty, value); } + } + + public string PlaceholderText + { + get { return (string)GetValue(PlaceholderTextProperty); } + set { SetValue(PlaceholderTextProperty, value); } + } + + public Brush PlaceholderForegroundBrush + { + get { return (Brush)GetValue(PlaceholderForegroundBrushProperty); } + set { SetValue(PlaceholderForegroundBrushProperty, value); } + } + + public new string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + protected internal string DisabledText + { + get { return (string)GetValue(DisabledTextProperty); } + set { SetValue(DisabledTextProperty, value); } + } + + static InputScope PasswordInputScope + { + get + { + if (s_passwordInputScope != null) + return s_passwordInputScope; + + s_passwordInputScope = new InputScope(); + var name = new InputScopeName { NameValue = InputScopeNameValue.Default }; + s_passwordInputScope.Names.Add(name); + + return s_passwordInputScope; + } + } + + void DelayObfuscation() + { + int lengthDifference = base.Text.Length - Text.Length; + + var savedSelectionStart = SelectionStart; + string updatedRealText = DetermineTextFromPassword(Text, SelectionStart, base.Text); + + if (Text == updatedRealText) + { + // Nothing to do + return; + } + + _internalChangeFlag = true; + Text = updatedRealText; + _internalChangeFlag = false; + + // Cancel any pending delayed obfuscation + _cts?.Cancel(); + _cts = null; + + string newText; + + if (lengthDifference != 1) + { + // Either More than one character got added in this text change (e.g., a paste operation) + // Or characters were removed. Either way, we don't need to do the delayed obfuscation dance + newText = Obfuscate(Text); + } + else + { + // Only one character was added; we need to leave it visible for a brief time period + // Obfuscate all but the character added for now + newText = Obfuscate(Text, savedSelectionStart - 1); + + // Leave the added character visible until a new character is added + // or sufficient time has passed + if (_cts == null) + { + _cts = new CancellationTokenSource(); + } + + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(0.5), _cts.Token); + _cts.Token.ThrowIfCancellationRequested(); + await Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => + { + var ss = SelectionStart; + var sl = SelectionLength; + base.Text = Obfuscate(Text); + SelectionStart = ss; + SelectionLength = sl; + })); + }, _cts.Token); + } + + if (base.Text != newText) + { + base.Text = newText; + } + SelectionStart = savedSelectionStart; + } + + static string DetermineTextFromPassword(string realText, int start, string passwordText) + { + var lengthDifference = passwordText.Length - realText.Length; + if (lengthDifference > 0) + realText = realText.Insert(start - lengthDifference, new string(ObfuscationCharacter, lengthDifference)); + else if (lengthDifference < 0) + realText = realText.Remove(start, -lengthDifference); + + var sb = new System.Text.StringBuilder(passwordText.Length); + for (int i = 0; i < passwordText.Length; i++) + sb.Append(passwordText[i] == ObfuscationCharacter ? realText[i] : passwordText[i]); + + return sb.ToString(); + } + + string Obfuscate(string text, int visibleSymbolIndex = -1) + { + if (visibleSymbolIndex == -1) + return new string(ObfuscationCharacter, text?.Length ?? 0); + + if (text == null || text.Length == 1) + return text; + var prefix = visibleSymbolIndex > 0 ? new string(ObfuscationCharacter, visibleSymbolIndex) : string.Empty; + var suffix = visibleSymbolIndex == text.Length - 1 + ? string.Empty + : new string(ObfuscationCharacter, text.Length - visibleSymbolIndex - 1); + + return prefix + text.Substring(visibleSymbolIndex, 1) + suffix; + } + + static void OnIsPasswordChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) + { + var textBox = (MauiTextBox)dependencyObject; + textBox.UpdateInputScope(); + textBox.SyncBaseText(); + } + + void OnSelectionChanged(object sender, RoutedEventArgs routedEventArgs) + { + // Cache this value for later use as explained in OnKeyDown below + _cachedSelectionLength = SelectionLength; + } + + // Because the implementation of a password entry is based around inheriting from TextBox (via FormsTextBox), there + // are some inaccuracies in the behavior. OnKeyDown is what needs to be used for a workaround in this case because + // there's no easy way to disable specific keyboard shortcuts in a TextBox, so key presses are being intercepted and + // handled accordingly. + protected override void OnKeyDown(KeyEventArgs e) + { + if (IsPassword) + { + // The ctrlDown flag is used to track if the Ctrl key is pressed; if it's actively being used and the most recent + // key to trigger OnKeyDown, then treat it as handled. + var ctrlDown = (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) && e.IsDown; + + // The shift, tab, and directional (Home/End/PgUp/PgDown included) keys can be used to select text and should otherwise + // be ignored. + if ( + e.Key == Key.LeftShift || + e.Key == Key.RightShift || + e.Key == Key.Tab || + e.Key == Key.Left || + e.Key == Key.Right || + e.Key == Key.Up || + e.Key == Key.Down || + e.Key == Key.Home || + e.Key == Key.End || + e.Key == Key.PageUp || + e.Key == Key.PageDown) + { + base.OnKeyDown(e); + return; + } + // For anything else, continue on (calling base.OnKeyDown) and then if Ctrl is still being pressed, do nothing about it. + // The tricky part here is that the SelectionLength value needs to be cached because in an example where the user entered + // '123' into the field and selects all of it, the moment that any character key is pressed to replace the entire string, + // the SelectionLength is equal to zero, which is not what's desired. Entering a key will thus remove the selected number + // of characters from the Text value. OnKeyDown is fortunately called before OnSelectionChanged which enables this. + else + { + // If the C or X keys (copy/cut) are pressed while Ctrl is active, ignore handing them at all. Undo and Redo (Z/Y) should + // be ignored as well as this emulates the regular behavior of a PasswordBox. + if ((e.Key == Key.C || e.Key == Key.X || e.Key == Key.Z || e.Key == Key.Y) && ctrlDown) + { + e.Handled = false; + return; + } + + base.OnKeyDown(e); + if (_cachedSelectionLength > 0 && !ctrlDown) + { + var savedSelectionStart = SelectionStart; + Text = Text.Remove(SelectionStart, _cachedSelectionLength); + SelectionStart = savedSelectionStart; + } + } + } + else + base.OnKeyDown(e); + } + + void OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs) + { + if (IsPassword) + { + DelayObfuscation(); + } + else if (base.Text != Text) + { + // Not in password mode, so we just need to make the "real" Text match + // what's in the textbox; the internalChange flag keeps the TextProperty + // synchronization from happening + _internalChangeFlag = true; + Text = base.Text; + _internalChangeFlag = false; + } + } + + void SyncBaseText() + { + if (_internalChangeFlag) + return; + var savedSelectionStart = SelectionStart; + base.Text = IsPassword ? Obfuscate(Text) : Text; + DisabledText = base.Text; + var len = base.Text.Length; + SelectionStart = savedSelectionStart > len ? len : savedSelectionStart; + } + + static void TextPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) + { + var textBox = (MauiTextBox)dependencyObject; + textBox.SyncBaseText(); + } + + void UpdateInputScope() + { + if (IsPassword) + { + _cachedInputScope = InputScope; + InputScope = PasswordInputScope; // We don't want suggestions turned on if we're in password mode + } + else + InputScope = _cachedInputScope; + } + } +} diff --git a/Maui.Core/Controls/MauiTimePicker.Win32.cs b/Maui.Core/Controls/MauiTimePicker.Win32.cs new file mode 100644 index 000000000000..2da310d57a8e --- /dev/null +++ b/Maui.Core/Controls/MauiTimePicker.Win32.cs @@ -0,0 +1,141 @@ +using System.Windows; +using System.Windows.Controls; + +namespace System.Maui.Core.Controls +{ + public class MauiTimePicker : TextBox + { + #region Properties + public static readonly DependencyProperty TimeProperty = DependencyProperty.Register("Time", typeof(TimeSpan?), typeof(MauiTimePicker), new PropertyMetadata(null, new PropertyChangedCallback(OnTimePropertyChanged))); + public TimeSpan? Time + { + get { return (TimeSpan?)GetValue(TimeProperty); } + set { SetValue(TimeProperty, value); } + } + + public static readonly DependencyProperty TimeFormatProperty = DependencyProperty.Register("TimeFormat", typeof(String), typeof(MauiTimePicker), new PropertyMetadata(@"hh\:mm", new PropertyChangedCallback(OnTimeFormatPropertyChanged))); + public String TimeFormat + { + get { return (String)GetValue(TimeFormatProperty); } + set { SetValue(TimeFormatProperty, value); } + } + #endregion + + #region Events + public delegate void TimeChangedEventHandler(object sender, TimeChangedEventArgs e); + public event TimeChangedEventHandler TimeChanged; + #endregion + + public MauiTimePicker() + { + + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + SetText(); + } + + private void SetText() + { + if (Time == null) + Text = null; + else + { + var dateTime = new DateTime(Time.Value.Ticks); + + String text = dateTime.ToString(String.IsNullOrWhiteSpace(TimeFormat) ? @"hh\:mm" : TimeFormat.ToLower()); + if (text.CompareTo(Text) != 0) + Text = text; + } + } + + private void SetTime() + { + DateTime dateTime = DateTime.MinValue; + String timeFormat = String.IsNullOrWhiteSpace(TimeFormat) ? @"hh\:mm" : TimeFormat.ToLower(); + + if (DateTime.TryParseExact(Text, timeFormat, null, System.Globalization.DateTimeStyles.None, out dateTime)) + { + if ((Time == null) || (Time != null && Time.Value.CompareTo(dateTime.TimeOfDay) != 0)) + { + if (dateTime.TimeOfDay < TimeSpan.FromHours(24) && dateTime.TimeOfDay > TimeSpan.Zero) + Time = dateTime.TimeOfDay; + else + SetText(); + } + } + else + SetText(); + } + + #region Overrides + protected override void OnLostFocus(RoutedEventArgs e) + { + SetTime(); + base.OnLostFocus(e); + } + + protected override void OnGotFocus(RoutedEventArgs e) + { + base.OnGotFocus(e); + } + #endregion + + #region Property Changes + private static void OnTimePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + MauiTimePicker element = d as MauiTimePicker; + if (element == null) + return; + + element.OnTimeChanged(e.OldValue as TimeSpan?, e.NewValue as TimeSpan?); + } + + private void OnTimeChanged(TimeSpan? oldValue, TimeSpan? newValue) + { + SetText(); + + if (TimeChanged != null) + TimeChanged(this, new TimeChangedEventArgs(oldValue, newValue)); + } + + private static void OnTimeFormatPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + MauiTimePicker element = d as MauiTimePicker; + if (element == null) + return; + + element.OnTimeFormatChanged(); + } + + private void OnTimeFormatChanged() + { + SetText(); + } + #endregion + } + + public class TimeChangedEventArgs : EventArgs + { + private TimeSpan? _oldTime; + private TimeSpan? _newTime; + + public TimeSpan? OldTime + { + get { return _oldTime; } + } + + public TimeSpan? NewTime + { + get { return _newTime; } + } + + public TimeChangedEventArgs(TimeSpan? oldTime, TimeSpan? newTime) + { + _oldTime = oldTime; + _newTime = newTime; + } + } +} diff --git a/Maui.Core/Extensions/NumericExtensions.cs b/Maui.Core/Extensions/NumericExtensions.cs new file mode 100644 index 000000000000..a77b1265af59 --- /dev/null +++ b/Maui.Core/Extensions/NumericExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace System.Maui.Extensions +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class NumericExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(this double self, double min, double max) + { + if (max < min) + { + return max; + } + else if (self < min) + { + return min; + } + else if (self > max) + { + return max; + } + + return self; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(this int self, int min, int max) + { + if (max < min) + { + return max; + } + else if (self < min) + { + return min; + } + else if (self > max) + { + return max; + } + + return self; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Extensions/RectExtensions.cs b/Maui.Core/Extensions/RectExtensions.cs new file mode 100644 index 000000000000..5fd0168a84c0 --- /dev/null +++ b/Maui.Core/Extensions/RectExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace System.Maui.Core +{ + public static class RectExtensions + { + public static Point Center(this Rectangle rectangle) => new Point(rectangle.X + (rectangle.Width / 2), rectangle.Y + (rectangle.Height / 2)); + public static void Center(this ref Rectangle rectangle, Point center) + { + var halfWidth = rectangle.Width / 2; + var halfHeight = rectangle.Height / 2; + rectangle.X = center.X - halfWidth; + rectangle.Y = center.Y - halfHeight; + } + public static bool BoundsContains(this Rectangle rect, Point point) => + point.X >= 0 && point.X <= rect.Width && + point.Y >= 0 && point.Y <= rect.Height; + + public static bool Contains(this Rectangle rect, Point[] points) + => points.Any(x => rect.Contains(x)); + + //public static Rectangle ApplyPadding(this Rectangle rect, Thickness thickness) + //{ + // if (thickness == null) + // return rect; + // rect.X += thickness.Left; + // rect.Y += thickness.Top; + // rect.Width -= thickness.HorizontalThickness; + // rect.Height -= thickness.VerticalThickness; + + // return rect; + //} + } +} diff --git a/Maui.Core/Forms/Color.cs b/Maui.Core/Forms/Color.cs new file mode 100644 index 000000000000..111db63f88ea --- /dev/null +++ b/Maui.Core/Forms/Color.cs @@ -0,0 +1,587 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Maui.Extensions; + +namespace System.Maui +{ + [DebuggerDisplay("R={R}, G={G}, B={B}, A={A}, Hue={Hue}, Saturation={Saturation}, Luminosity={Luminosity}")] + [TypeConverter (typeof(Xaml.ColorTypeConverter))] + public struct Color + { + readonly Mode _mode; + + enum Mode + { + Default, + Rgb, + Hsl + } + + public static Color Default + { + get { return new Color(-1d, -1d, -1d, -1d, Mode.Default); } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsDefault + { + get { return _mode == Mode.Default; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static void SetAccent(Color value) => Accent = value; + public static Color Accent { get; internal set; } + + readonly float _a; + + public double A + { + get { return _a; } + } + + readonly float _r; + + public double R + { + get { return _r; } + } + + readonly float _g; + + public double G + { + get { return _g; } + } + + readonly float _b; + + public double B + { + get { return _b; } + } + + readonly float _hue; + + public double Hue + { + get { return _hue; } + } + + readonly float _saturation; + + public double Saturation + { + get { return _saturation; } + } + + readonly float _luminosity; + + public double Luminosity + { + get { return _luminosity; } + } + + public Color(double r, double g, double b, double a) : this(r, g, b, a, Mode.Rgb) + { + } + + Color(double w, double x, double y, double z, Mode mode) + { + _mode = mode; + switch (mode) + { + default: + case Mode.Default: + _r = _g = _b = _a = -1; + _hue = _saturation = _luminosity = -1; + break; + case Mode.Rgb: + _r = (float)w.Clamp(0, 1); + _g = (float)x.Clamp(0, 1); + _b = (float)y.Clamp(0, 1); + _a = (float)z.Clamp(0, 1); + ConvertToHsl(_r, _g, _b, mode, out _hue, out _saturation, out _luminosity); + break; + case Mode.Hsl: + _hue = (float)w.Clamp(0, 1); + _saturation = (float)x.Clamp(0, 1); + _luminosity = (float)y.Clamp(0, 1); + _a = (float)z.Clamp(0, 1); + ConvertToRgb(_hue, _saturation, _luminosity, mode, out _r, out _g, out _b); + break; + } + } + + Color(int r, int g, int b) + { + _mode = Mode.Rgb; + _r = r / 255f; + _g = g / 255f; + _b = b / 255f; + _a = 1; + ConvertToHsl(_r, _g, _b, _mode, out _hue, out _saturation, out _luminosity); + } + + Color(int r, int g, int b, int a) + { + _mode = Mode.Rgb; + _r = r / 255f; + _g = g / 255f; + _b = b / 255f; + _a = a / 255f; + ConvertToHsl(_r, _g, _b, _mode, out _hue, out _saturation, out _luminosity); + } + + public Color(double r, double g, double b) : this(r, g, b, 1) + { + } + + public Color(double value) : this(value, value, value, 1) + { + } + + public Color MultiplyAlpha(double alpha) + { + switch (_mode) + { + default: + case Mode.Default: + throw new InvalidOperationException("Invalid on Color.Default"); + case Mode.Rgb: + return new Color(_r, _g, _b, _a * alpha, Mode.Rgb); + case Mode.Hsl: + return new Color(_hue, _saturation, _luminosity, _a * alpha, Mode.Hsl); + } + } + + public Color AddLuminosity(double delta) + { + if (_mode == Mode.Default) + throw new InvalidOperationException("Invalid on Color.Default"); + + return new Color(_hue, _saturation, _luminosity + delta, _a, Mode.Hsl); + } + + public Color WithHue(double hue) + { + if (_mode == Mode.Default) + throw new InvalidOperationException("Invalid on Color.Default"); + return new Color(hue, _saturation, _luminosity, _a, Mode.Hsl); + } + + public Color WithSaturation(double saturation) + { + if (_mode == Mode.Default) + throw new InvalidOperationException("Invalid on Color.Default"); + return new Color(_hue, saturation, _luminosity, _a, Mode.Hsl); + } + + public Color WithLuminosity(double luminosity) + { + if (_mode == Mode.Default) + throw new InvalidOperationException("Invalid on Color.Default"); + return new Color(_hue, _saturation, luminosity, _a, Mode.Hsl); + } + + static void ConvertToRgb(float hue, float saturation, float luminosity, Mode mode, out float r, out float g, out float b) + { + if (mode != Mode.Hsl) + throw new InvalidOperationException(); + + if (luminosity == 0) + { + r = g = b = 0; + return; + } + + if (saturation == 0) + { + r = g = b = luminosity; + return; + } + float temp2 = luminosity <= 0.5f ? luminosity * (1.0f + saturation) : luminosity + saturation - luminosity * saturation; + float temp1 = 2.0f * luminosity - temp2; + + var t3 = new[] { hue + 1.0f / 3.0f, hue, hue - 1.0f / 3.0f }; + var clr = new float[] { 0, 0, 0 }; + for (var i = 0; i < 3; i++) + { + if (t3[i] < 0) + t3[i] += 1.0f; + if (t3[i] > 1) + t3[i] -= 1.0f; + if (6.0 * t3[i] < 1.0) + clr[i] = temp1 + (temp2 - temp1) * t3[i] * 6.0f; + else if (2.0 * t3[i] < 1.0) + clr[i] = temp2; + else if (3.0 * t3[i] < 2.0) + clr[i] = temp1 + (temp2 - temp1) * (2.0f / 3.0f - t3[i]) * 6.0f; + else + clr[i] = temp1; + } + + r = clr[0]; + g = clr[1]; + b = clr[2]; + } + + static void ConvertToHsl(float r, float g, float b, Mode mode, out float h, out float s, out float l) + { + float v = Math.Max(r, g); + v = Math.Max(v, b); + + float m = Math.Min(r, g); + m = Math.Min(m, b); + + l = (m + v) / 2.0f; + if (l <= 0.0) + { + h = s = l = 0; + return; + } + float vm = v - m; + s = vm; + + if (s > 0.0) + { + s /= l <= 0.5f ? v + m : 2.0f - v - m; + } + else + { + h = 0; + s = 0; + return; + } + + float r2 = (v - r) / vm; + float g2 = (v - g) / vm; + float b2 = (v - b) / vm; + + if (r == v) + { + h = g == m ? 5.0f + b2 : 1.0f - g2; + } + else if (g == v) + { + h = b == m ? 1.0f + r2 : 3.0f - b2; + } + else + { + h = r == m ? 3.0f + g2 : 5.0f - r2; + } + h /= 6.0f; + } + + public static bool operator ==(Color color1, Color color2) + { + return EqualsInner(color1, color2); + } + + public static bool operator !=(Color color1, Color color2) + { + return !EqualsInner(color1, color2); + } + + public override int GetHashCode() + { + unchecked + { + int hashcode = _r.GetHashCode(); + hashcode = (hashcode * 397) ^ _g.GetHashCode(); + hashcode = (hashcode * 397) ^ _b.GetHashCode(); + hashcode = (hashcode * 397) ^ _a.GetHashCode(); + return hashcode; + } + } + + public override bool Equals(object obj) + { + if (obj is Color) + { + return EqualsInner(this, (Color)obj); + } + return base.Equals(obj); + } + + static bool EqualsInner(Color color1, Color color2) + { + if (color1._mode == Mode.Default && color2._mode == Mode.Default) + return true; + if (color1._mode == Mode.Default || color2._mode == Mode.Default) + return false; + if (color1._mode == Mode.Hsl && color2._mode == Mode.Hsl) + return color1._hue == color2._hue && color1._saturation == color2._saturation && color1._luminosity == color2._luminosity && color1._a == color2._a; + return color1._r == color2._r && color1._g == color2._g && color1._b == color2._b && color1._a == color2._a; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Color: A={0}, R={1}, G={2}, B={3}, Hue={4}, Saturation={5}, Luminosity={6}]", A, R, G, B, Hue, Saturation, Luminosity); + } + + public string ToHex() + { + var red = (uint)(R * 255); + var green = (uint)(G * 255); + var blue = (uint)(B * 255); + var alpha = (uint)(A * 255); + return $"#{alpha:X2}{red:X2}{green:X2}{blue:X2}"; + } + + static uint ToHex (char c) + { + ushort x = (ushort)c; + if (x >= '0' && x <= '9') + return (uint)(x - '0'); + + x |= 0x20; + if (x >= 'a' && x <= 'f') + return (uint)(x - 'a' + 10); + return 0; + } + + static uint ToHexD (char c) + { + var j = ToHex (c); + return (j << 4) | j; + } + + public static Color FromHex (string hex) + { + // Undefined + if (hex.Length < 3) + return Default; + int idx = (hex [0] == '#') ? 1 : 0; + + switch (hex.Length - idx) { + case 3: //#rgb => ffrrggbb + var t1 = ToHexD (hex [idx++]); + var t2 = ToHexD (hex [idx++]); + var t3 = ToHexD (hex [idx]); + + return FromRgb ((int)t1, (int)t2, (int)t3); + + case 4: //#argb => aarrggbb + var f1 = ToHexD (hex [idx++]); + var f2 = ToHexD (hex [idx++]); + var f3 = ToHexD (hex [idx++]); + var f4 = ToHexD (hex [idx]); + return FromRgba ((int)f2, (int)f3, (int)f4, (int)f1); + + case 6: //#rrggbb => ffrrggbb + return FromRgb ((int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx++])), + (int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx++])), + (int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx]))); + + case 8: //#aarrggbb + var a1 = ToHex (hex [idx++]) << 4 | ToHex (hex [idx++]); + return FromRgba ((int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx++])), + (int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx++])), + (int)(ToHex (hex [idx++]) << 4 | ToHex (hex [idx])), + (int)a1); + + default: //everything else will result in unexpected results + return Default; + } + } + + public static Color FromUint(uint argb) + { + return FromRgba((byte)((argb & 0x00ff0000) >> 0x10), (byte)((argb & 0x0000ff00) >> 0x8), (byte)(argb & 0x000000ff), (byte)((argb & 0xff000000) >> 0x18)); + } + + public static Color FromRgba(int r, int g, int b, int a) + { + double red = (double)r / 255; + double green = (double)g / 255; + double blue = (double)b / 255; + double alpha = (double)a / 255; + return new Color(red, green, blue, alpha, Mode.Rgb); + } + + public static Color FromRgb(int r, int g, int b) + { + return FromRgba(r, g, b, 255); + } + + public static Color FromRgba(double r, double g, double b, double a) + { + return new Color(r, g, b, a); + } + + public static Color FromRgb(double r, double g, double b) + { + return new Color(r, g, b, 1d, Mode.Rgb); + } + + public static Color FromHsla(double h, double s, double l, double a = 1d) + { + return new Color(h, s, l, a, Mode.Hsl); + } +#if !NETSTANDARD1_0 + public static implicit operator System.Drawing.Color(Color color) + { + if (color.IsDefault) + return System.Drawing.Color.Empty; + return System.Drawing.Color.FromArgb((byte)(color._a * 255), (byte)(color._r * 255), (byte)(color._g * 255), (byte)(color._b * 255)); + } + + public static implicit operator Color(System.Drawing.Color color) + { + if (color.IsEmpty) + return Color.Default; + return FromRgba(color.R, color.G, color.B, color.A); + } +#endif + #region Color Definitions + + // matches colors in WPF's System.Windows.Media.Colors + public static readonly Color AliceBlue = new Color(240, 248, 255); + public static readonly Color AntiqueWhite = new Color(250, 235, 215); + public static readonly Color Aqua = new Color(0, 255, 255); + public static readonly Color Aquamarine = new Color(127, 255, 212); + public static readonly Color Azure = new Color(240, 255, 255); + public static readonly Color Beige = new Color(245, 245, 220); + public static readonly Color Bisque = new Color(255, 228, 196); + public static readonly Color Black = new Color(0, 0, 0); + public static readonly Color BlanchedAlmond = new Color(255, 235, 205); + public static readonly Color Blue = new Color(0, 0, 255); + public static readonly Color BlueViolet = new Color(138, 43, 226); + public static readonly Color Brown = new Color(165, 42, 42); + public static readonly Color BurlyWood = new Color(222, 184, 135); + public static readonly Color CadetBlue = new Color(95, 158, 160); + public static readonly Color Chartreuse = new Color(127, 255, 0); + public static readonly Color Chocolate = new Color(210, 105, 30); + public static readonly Color Coral = new Color(255, 127, 80); + public static readonly Color CornflowerBlue = new Color(100, 149, 237); + public static readonly Color Cornsilk = new Color(255, 248, 220); + public static readonly Color Crimson = new Color(220, 20, 60); + public static readonly Color Cyan = new Color(0, 255, 255); + public static readonly Color DarkBlue = new Color(0, 0, 139); + public static readonly Color DarkCyan = new Color(0, 139, 139); + public static readonly Color DarkGoldenrod = new Color(184, 134, 11); + public static readonly Color DarkGray = new Color(169, 169, 169); + public static readonly Color DarkGreen = new Color(0, 100, 0); + public static readonly Color DarkKhaki = new Color(189, 183, 107); + public static readonly Color DarkMagenta = new Color(139, 0, 139); + public static readonly Color DarkOliveGreen = new Color(85, 107, 47); + public static readonly Color DarkOrange = new Color(255, 140, 0); + public static readonly Color DarkOrchid = new Color(153, 50, 204); + public static readonly Color DarkRed = new Color(139, 0, 0); + public static readonly Color DarkSalmon = new Color(233, 150, 122); + public static readonly Color DarkSeaGreen = new Color(143, 188, 143); + public static readonly Color DarkSlateBlue = new Color(72, 61, 139); + public static readonly Color DarkSlateGray = new Color(47, 79, 79); + public static readonly Color DarkTurquoise = new Color(0, 206, 209); + public static readonly Color DarkViolet = new Color(148, 0, 211); + public static readonly Color DeepPink = new Color(255, 20, 147); + public static readonly Color DeepSkyBlue = new Color(0, 191, 255); + public static readonly Color DimGray = new Color(105, 105, 105); + public static readonly Color DodgerBlue = new Color(30, 144, 255); + public static readonly Color Firebrick = new Color(178, 34, 34); + public static readonly Color FloralWhite = new Color(255, 250, 240); + public static readonly Color ForestGreen = new Color(34, 139, 34); + public static readonly Color Fuchsia = new Color(255, 0, 255); + [Obsolete("Fuschia is obsolete as of version 1.3.0. Please use Fuchsia instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly Color Fuschia = new Color(255, 0, 255); + public static readonly Color Gainsboro = new Color(220, 220, 220); + public static readonly Color GhostWhite = new Color(248, 248, 255); + public static readonly Color Gold = new Color(255, 215, 0); + public static readonly Color Goldenrod = new Color(218, 165, 32); + public static readonly Color Gray = new Color(128, 128, 128); + public static readonly Color Green = new Color(0, 128, 0); + public static readonly Color GreenYellow = new Color(173, 255, 47); + public static readonly Color Honeydew = new Color(240, 255, 240); + public static readonly Color HotPink = new Color(255, 105, 180); + public static readonly Color IndianRed = new Color(205, 92, 92); + public static readonly Color Indigo = new Color(75, 0, 130); + public static readonly Color Ivory = new Color(255, 255, 240); + public static readonly Color Khaki = new Color(240, 230, 140); + public static readonly Color Lavender = new Color(230, 230, 250); + public static readonly Color LavenderBlush = new Color(255, 240, 245); + public static readonly Color LawnGreen = new Color(124, 252, 0); + public static readonly Color LemonChiffon = new Color(255, 250, 205); + public static readonly Color LightBlue = new Color(173, 216, 230); + public static readonly Color LightCoral = new Color(240, 128, 128); + public static readonly Color LightCyan = new Color(224, 255, 255); + public static readonly Color LightGoldenrodYellow = new Color(250, 250, 210); + public static readonly Color LightGray = new Color(211, 211, 211); + public static readonly Color LightGreen = new Color(144, 238, 144); + public static readonly Color LightPink = new Color(255, 182, 193); + public static readonly Color LightSalmon = new Color(255, 160, 122); + public static readonly Color LightSeaGreen = new Color(32, 178, 170); + public static readonly Color LightSkyBlue = new Color(135, 206, 250); + public static readonly Color LightSlateGray = new Color(119, 136, 153); + public static readonly Color LightSteelBlue = new Color(176, 196, 222); + public static readonly Color LightYellow = new Color(255, 255, 224); + public static readonly Color Lime = new Color(0, 255, 0); + public static readonly Color LimeGreen = new Color(50, 205, 50); + public static readonly Color Linen = new Color(250, 240, 230); + public static readonly Color Magenta = new Color(255, 0, 255); + public static readonly Color Maroon = new Color(128, 0, 0); + public static readonly Color MediumAquamarine = new Color(102, 205, 170); + public static readonly Color MediumBlue = new Color(0, 0, 205); + public static readonly Color MediumOrchid = new Color(186, 85, 211); + public static readonly Color MediumPurple = new Color(147, 112, 219); + public static readonly Color MediumSeaGreen = new Color(60, 179, 113); + public static readonly Color MediumSlateBlue = new Color(123, 104, 238); + public static readonly Color MediumSpringGreen = new Color(0, 250, 154); + public static readonly Color MediumTurquoise = new Color(72, 209, 204); + public static readonly Color MediumVioletRed = new Color(199, 21, 133); + public static readonly Color MidnightBlue = new Color(25, 25, 112); + public static readonly Color MintCream = new Color(245, 255, 250); + public static readonly Color MistyRose = new Color(255, 228, 225); + public static readonly Color Moccasin = new Color(255, 228, 181); + public static readonly Color NavajoWhite = new Color(255, 222, 173); + public static readonly Color Navy = new Color(0, 0, 128); + public static readonly Color OldLace = new Color(253, 245, 230); + public static readonly Color Olive = new Color(128, 128, 0); + public static readonly Color OliveDrab = new Color(107, 142, 35); + public static readonly Color Orange = new Color(255, 165, 0); + public static readonly Color OrangeRed = new Color(255, 69, 0); + public static readonly Color Orchid = new Color(218, 112, 214); + public static readonly Color PaleGoldenrod = new Color(238, 232, 170); + public static readonly Color PaleGreen = new Color(152, 251, 152); + public static readonly Color PaleTurquoise = new Color(175, 238, 238); + public static readonly Color PaleVioletRed = new Color(219, 112, 147); + public static readonly Color PapayaWhip = new Color(255, 239, 213); + public static readonly Color PeachPuff = new Color(255, 218, 185); + public static readonly Color Peru = new Color(205, 133, 63); + public static readonly Color Pink = new Color(255, 192, 203); + public static readonly Color Plum = new Color(221, 160, 221); + public static readonly Color PowderBlue = new Color(176, 224, 230); + public static readonly Color Purple = new Color(128, 0, 128); + public static readonly Color Red = new Color(255, 0, 0); + public static readonly Color RosyBrown = new Color(188, 143, 143); + public static readonly Color RoyalBlue = new Color(65, 105, 225); + public static readonly Color SaddleBrown = new Color(139, 69, 19); + public static readonly Color Salmon = new Color(250, 128, 114); + public static readonly Color SandyBrown = new Color(244, 164, 96); + public static readonly Color SeaGreen = new Color(46, 139, 87); + public static readonly Color SeaShell = new Color(255, 245, 238); + public static readonly Color Sienna = new Color(160, 82, 45); + public static readonly Color Silver = new Color(192, 192, 192); + public static readonly Color SkyBlue = new Color(135, 206, 235); + public static readonly Color SlateBlue = new Color(106, 90, 205); + public static readonly Color SlateGray = new Color(112, 128, 144); + public static readonly Color Snow = new Color(255, 250, 250); + public static readonly Color SpringGreen = new Color(0, 255, 127); + public static readonly Color SteelBlue = new Color(70, 130, 180); + public static readonly Color Tan = new Color(210, 180, 140); + public static readonly Color Teal = new Color(0, 128, 128); + public static readonly Color Thistle = new Color(216, 191, 216); + public static readonly Color Tomato = new Color(255, 99, 71); + public static readonly Color Transparent = new Color(255, 255, 255, 0); + public static readonly Color Turquoise = new Color(64, 224, 208); + public static readonly Color Violet = new Color(238, 130, 238); + public static readonly Color Wheat = new Color(245, 222, 179); + public static readonly Color White = new Color(255, 255, 255); + public static readonly Color WhiteSmoke = new Color(245, 245, 245); + public static readonly Color Yellow = new Color(255, 255, 0); + public static readonly Color YellowGreen = new Color(154, 205, 50); + + #endregion + } +} diff --git a/Maui.Core/Forms/EditorAutoSizeOption.cs b/Maui.Core/Forms/EditorAutoSizeOption.cs new file mode 100644 index 000000000000..2c1d11fddfbe --- /dev/null +++ b/Maui.Core/Forms/EditorAutoSizeOption.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public enum EditorAutoSizeOption + { + Disabled = 0, + TextChanges = 1 + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/EvalRequested.cs b/Maui.Core/Forms/EvalRequested.cs new file mode 100644 index 000000000000..0ec9fb9ab360 --- /dev/null +++ b/Maui.Core/Forms/EvalRequested.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; +using System.Threading.Tasks; + +namespace System.Maui +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public delegate Task EvaluateJavaScriptDelegate(string script); + + [EditorBrowsable(EditorBrowsableState.Never)] + public class EvalRequested : EventArgs + { + public string Script { get; } + + public EvalRequested(string script) + { + Script = script; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/IDispatcher.cs b/Maui.Core/Forms/IDispatcher.cs new file mode 100644 index 000000000000..2ed39cddd6ff --- /dev/null +++ b/Maui.Core/Forms/IDispatcher.cs @@ -0,0 +1,10 @@ +using System; + +namespace System.Maui +{ + public interface IDispatcher + { + void BeginInvokeOnMainThread(Action action); + bool IsInvokeRequired { get; } + } +} diff --git a/Maui.Core/Forms/IRegisterable.cs b/Maui.Core/Forms/IRegisterable.cs new file mode 100644 index 000000000000..ce120d13f5c8 --- /dev/null +++ b/Maui.Core/Forms/IRegisterable.cs @@ -0,0 +1,6 @@ +namespace System.Maui +{ + public interface IRegisterable + { + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/IWebViewDelegate.cs b/Maui.Core/Forms/IWebViewDelegate.cs new file mode 100644 index 000000000000..42a0ca70af78 --- /dev/null +++ b/Maui.Core/Forms/IWebViewDelegate.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface IWebViewDelegate + { + void LoadHtml(string html, string baseUrl); + void LoadUrl(string url); + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/LockableObservableListWrapper.cs b/Maui.Core/Forms/LockableObservableListWrapper.cs new file mode 100644 index 000000000000..503f68167da4 --- /dev/null +++ b/Maui.Core/Forms/LockableObservableListWrapper.cs @@ -0,0 +1,134 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace System.Maui +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class LockableObservableListWrapper : IList, ICollection, INotifyCollectionChanged, INotifyPropertyChanged, IReadOnlyList, IReadOnlyCollection, IEnumerable, IEnumerable + { + public readonly ObservableCollection _list = new ObservableCollection(); + + event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged + { + add { ((INotifyCollectionChanged)_list).CollectionChanged += value; } + remove { ((INotifyCollectionChanged)_list).CollectionChanged -= value; } + } + + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add { ((INotifyPropertyChanged)_list).PropertyChanged += value; } + remove { ((INotifyPropertyChanged)_list).PropertyChanged -= value; } + } + + public bool IsLocked { get; set; } + + void ThrowOnLocked() + { + if (IsLocked) + throw new InvalidOperationException("The Items list can not be manipulated if the ItemsSource property is set"); + } + + public string this[int index] + { + get { return _list[index]; } + set + { + ThrowOnLocked(); + _list[index] = value; + } + } + + public int Count + { + get { return _list.Count; } + } + + public bool IsReadOnly + { + get { return ((IList)_list).IsReadOnly; } + } + + public void InternalAdd(string item) + { + _list.Add(item); + } + + public void Add(string item) + { + ThrowOnLocked(); + InternalAdd(item); + } + + public void InternalClear() + { + _list.Clear(); + } + + public void Clear() + { + ThrowOnLocked(); + InternalClear(); + } + + public bool Contains(string item) + { + return _list.Contains(item); + } + + public void CopyTo(string[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + public int IndexOf(string item) + { + return _list.IndexOf(item); + } + + public void InternalInsert(int index, string item) + { + _list.Insert(index, item); + } + + public void Insert(int index, string item) + { + ThrowOnLocked(); + InternalInsert(index, item); + } + + public bool InternalRemove(string item) + { + return _list.Remove(item); + } + + public bool Remove(string item) + { + ThrowOnLocked(); + return InternalRemove(item); + } + + public void InternalRemoveAt(int index) + { + _list.RemoveAt(index); + } + + public void RemoveAt(int index) + { + ThrowOnLocked(); + InternalRemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/Point.cs b/Maui.Core/Forms/Point.cs new file mode 100644 index 000000000000..3138efc61c3c --- /dev/null +++ b/Maui.Core/Forms/Point.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Maui.Xaml; + +namespace System.Maui +{ + [DebuggerDisplay("X={X}, Y={Y}")] + [TypeConverter(typeof(PointTypeConverter))] + public struct Point + { + public double X { get; set; } + + public double Y { get; set; } + + public static Point Zero = new Point(); + + public override string ToString() + { + return string.Format("{{X={0} Y={1}}}", X.ToString(CultureInfo.InvariantCulture), Y.ToString(CultureInfo.InvariantCulture)); + } + + public Point(double x, double y) : this() + { + X = x; + Y = y; + } + + public Point(Size sz) : this() + { + X = sz.Width; + Y = sz.Height; + } + + public override bool Equals(object o) + { + if (!(o is Point)) + return false; + + return this == (Point)o; + } + + public override int GetHashCode() + { + return X.GetHashCode() ^ (Y.GetHashCode() * 397); + } + + public Point Offset(double dx, double dy) + { + Point p = this; + p.X += dx; + p.Y += dy; + return p; + } + + public Point Round() + { + return new Point(Math.Round(X), Math.Round(Y)); + } + + public bool IsEmpty + { + get { return (X == 0) && (Y == 0); } + } + + public static explicit operator Size(Point pt) + { + return new Size(pt.X, pt.Y); + } + + public static Point operator +(Point pt, Size sz) + { + return new Point(pt.X + sz.Width, pt.Y + sz.Height); + } + + public static Point operator -(Point pt, Size sz) + { + return new Point(pt.X - sz.Width, pt.Y - sz.Height); + } + + public static bool operator ==(Point ptA, Point ptB) + { + return (ptA.X == ptB.X) && (ptA.Y == ptB.Y); + } + + public static bool operator !=(Point ptA, Point ptB) + { + return (ptA.X != ptB.X) || (ptA.Y != ptB.Y); + } + + public double Distance(Point other) + { + return Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2)); + } + + public void Deconstruct(out double x, out double y) + { + x = X; + y = Y; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/Rectangle.cs b/Maui.Core/Forms/Rectangle.cs new file mode 100644 index 000000000000..badcd8984482 --- /dev/null +++ b/Maui.Core/Forms/Rectangle.cs @@ -0,0 +1,227 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Maui.Xaml; + +namespace System.Maui +{ + [DebuggerDisplay("X={X}, Y={Y}, Width={Width}, Height={Height}")] + [TypeConverter(typeof(RectangleTypeConverter))] + public struct Rectangle + { + public double X { get; set; } + + public double Y { get; set; } + + public double Width { get; set; } + + public double Height { get; set; } + + public static Rectangle Zero = new Rectangle(); + + public override string ToString() + { + return string.Format("{{X={0} Y={1} Width={2} Height={3}}}", X.ToString(CultureInfo.InvariantCulture), Y.ToString(CultureInfo.InvariantCulture), Width.ToString(CultureInfo.InvariantCulture), + Height.ToString(CultureInfo.InvariantCulture)); + } + + // constructors + public Rectangle(double x, double y, double width, double height) : this() + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public Rectangle(Point loc, Size sz) : this(loc.X, loc.Y, sz.Width, sz.Height) + { + } + + public static Rectangle FromLTRB(double left, double top, double right, double bottom) + { + return new Rectangle(left, top, right - left, bottom - top); + } + + public bool Equals(Rectangle other) + { + return X.Equals(other.X) && Y.Equals(other.Y) && Width.Equals(other.Width) && Height.Equals(other.Height); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + return obj is Rectangle && Equals((Rectangle)obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Width.GetHashCode(); + hashCode = (hashCode * 397) ^ Height.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(Rectangle r1, Rectangle r2) + { + return (r1.Location == r2.Location) && (r1.Size == r2.Size); + } + + public static bool operator !=(Rectangle r1, Rectangle r2) + { + return !(r1 == r2); + } + + // Hit Testing / Intersection / Union + public bool Contains(Rectangle rect) + { + return X <= rect.X && Right >= rect.Right && Y <= rect.Y && Bottom >= rect.Bottom; + } + + public bool Contains(Point pt) + { + return Contains(pt.X, pt.Y); + } + + public bool Contains(double x, double y) + { + return (x >= Left) && (x < Right) && (y >= Top) && (y < Bottom); + } + + public bool IntersectsWith(Rectangle r) + { + return !((Left >= r.Right) || (Right <= r.Left) || (Top >= r.Bottom) || (Bottom <= r.Top)); + } + + public Rectangle Union(Rectangle r) + { + return Union(this, r); + } + + public static Rectangle Union(Rectangle r1, Rectangle r2) + { + return FromLTRB(Math.Min(r1.Left, r2.Left), Math.Min(r1.Top, r2.Top), Math.Max(r1.Right, r2.Right), Math.Max(r1.Bottom, r2.Bottom)); + } + + public Rectangle Intersect(Rectangle r) + { + return Intersect(this, r); + } + + public static Rectangle Intersect(Rectangle r1, Rectangle r2) + { + double x = Math.Max(r1.X, r2.X); + double y = Math.Max(r1.Y, r2.Y); + double width = Math.Min(r1.Right, r2.Right) - x; + double height = Math.Min(r1.Bottom, r2.Bottom) - y; + + if (width < 0 || height < 0) + { + return Zero; + } + return new Rectangle(x, y, width, height); + } + + // Position/Size + public double Top + { + get { return Y; } + set { Y = value; } + } + + public double Bottom + { + get { return Y + Height; } + set { Height = value - Y; } + } + + public double Right + { + get { return X + Width; } + set { Width = value - X; } + } + + public double Left + { + get { return X; } + set { X = value; } + } + + public bool IsEmpty + { + get { return (Width <= 0) || (Height <= 0); } + } + + public Size Size + { + get { return new Size(Width, Height); } + set + { + Width = value.Width; + Height = value.Height; + } + } + + public Point Location + { + get { return new Point(X, Y); } + set + { + X = value.X; + Y = value.Y; + } + } + + public Point Center + { + get { return new Point(X + Width / 2, Y + Height / 2); } + } + + // Inflate and Offset + public Rectangle Inflate(Size sz) + { + return Inflate(sz.Width, sz.Height); + } + + public Rectangle Inflate(double width, double height) + { + Rectangle r = this; + r.X -= width; + r.Y -= height; + r.Width += width * 2; + r.Height += height * 2; + return r; + } + + public Rectangle Offset(double dx, double dy) + { + Rectangle r = this; + r.X += dx; + r.Y += dy; + return r; + } + + public Rectangle Offset(Point dr) + { + return Offset(dr.X, dr.Y); + } + + public Rectangle Round() + { + return new Rectangle(Math.Round(X), Math.Round(Y), Math.Round(Width), Math.Round(Height)); + } + + public void Deconstruct(out double x, out double y, out double width, out double height) + { + x = X; + y = Y; + width = Width; + height = Height; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/Size.cs b/Maui.Core/Forms/Size.cs new file mode 100644 index 000000000000..04aaef56323f --- /dev/null +++ b/Maui.Core/Forms/Size.cs @@ -0,0 +1,117 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; + +namespace System.Maui +{ + [DebuggerDisplay("Width={Width}, Height={Height}")] + [TypeConverter (typeof(Xaml.SizeTypeConverter))] + public struct Size + { + double _width; + double _height; + + public static readonly Size Zero; + + public Size(double width, double height) + { + if (double.IsNaN(width)) + throw new ArgumentException("NaN is not a valid value for width"); + if (double.IsNaN(height)) + throw new ArgumentException("NaN is not a valid value for height"); + _width = width; + _height = height; + } + + public bool IsZero + { + get { return (_width == 0) && (_height == 0); } + } + + [DefaultValue(0d)] + public double Width + { + get { return _width; } + set + { + if (double.IsNaN(value)) + throw new ArgumentException("NaN is not a valid value for Width"); + _width = value; + } + } + + [DefaultValue(0d)] + public double Height + { + get { return _height; } + set + { + if (double.IsNaN(value)) + throw new ArgumentException("NaN is not a valid value for Height"); + _height = value; + } + } + + public static Size operator +(Size s1, Size s2) + { + return new Size(s1._width + s2._width, s1._height + s2._height); + } + + public static Size operator -(Size s1, Size s2) + { + return new Size(s1._width - s2._width, s1._height - s2._height); + } + + public static Size operator *(Size s1, double value) + { + return new Size(s1._width * value, s1._height * value); + } + + public static bool operator ==(Size s1, Size s2) + { + return (s1._width == s2._width) && (s1._height == s2._height); + } + + public static bool operator !=(Size s1, Size s2) + { + return (s1._width != s2._width) || (s1._height != s2._height); + } + + public static explicit operator Point(Size size) + { + return new Point(size.Width, size.Height); + } + + public bool Equals(Size other) + { + return _width.Equals(other._width) && _height.Equals(other._height); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + return obj is Size && Equals((Size)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_width.GetHashCode() * 397) ^ _height.GetHashCode(); + } + } + + public override string ToString() + { + return string.Format("{{Width={0} Height={1}}}", _width.ToString(CultureInfo.InvariantCulture), _height.ToString(CultureInfo.InvariantCulture)); + } + + public void Deconstruct(out double width, out double height) + { + width = Width; + height = Height; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/SizeRequest.cs b/Maui.Core/Forms/SizeRequest.cs new file mode 100644 index 000000000000..6686a3c71a83 --- /dev/null +++ b/Maui.Core/Forms/SizeRequest.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; + +namespace System.Maui +{ + [DebuggerDisplay("Request={Request.Width}x{Request.Height}, Minimum={Minimum.Width}x{Minimum.Height}")] + public struct SizeRequest + { + public Size Request { get; set; } + + public Size Minimum { get; set; } + + public SizeRequest(Size request, Size minimum) + { + Request = request; + Minimum = minimum; + } + + public SizeRequest(Size request) + { + Request = request; + Minimum = request; + } + + public override string ToString() + { + return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/TextType.cs b/Maui.Core/Forms/TextType.cs new file mode 100644 index 000000000000..3b9247cec4da --- /dev/null +++ b/Maui.Core/Forms/TextType.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public enum TextType + { + Text, + Html + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/UrlWebViewSource.cs b/Maui.Core/Forms/UrlWebViewSource.cs new file mode 100644 index 000000000000..a6f92984a9c4 --- /dev/null +++ b/Maui.Core/Forms/UrlWebViewSource.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; + +namespace System.Maui +{ + public class UrlWebViewSource : WebViewSource + { + public string Url { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public override void Load(IWebViewDelegate renderer) + { + renderer.LoadUrl(Url); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebNavigatedEventArgs.cs b/Maui.Core/Forms/WebNavigatedEventArgs.cs new file mode 100644 index 000000000000..f0a4820bff8e --- /dev/null +++ b/Maui.Core/Forms/WebNavigatedEventArgs.cs @@ -0,0 +1,12 @@ +namespace System.Maui +{ + public class WebNavigatedEventArgs : WebNavigationEventArgs + { + public WebNavigatedEventArgs(WebNavigationEvent navigationEvent, WebViewSource source, string url, WebNavigationResult result) : base(navigationEvent, source, url) + { + Result = result; + } + + public WebNavigationResult Result { get; private set; } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebNavigatingEventArgs.cs b/Maui.Core/Forms/WebNavigatingEventArgs.cs new file mode 100644 index 000000000000..23dd7bbd9829 --- /dev/null +++ b/Maui.Core/Forms/WebNavigatingEventArgs.cs @@ -0,0 +1,11 @@ +namespace System.Maui +{ + public class WebNavigatingEventArgs : WebNavigationEventArgs + { + public WebNavigatingEventArgs(WebNavigationEvent navigationEvent, WebViewSource source, string url) : base(navigationEvent, source, url) + { + } + + public bool Cancel { get; set; } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebNavigationEvent.cs b/Maui.Core/Forms/WebNavigationEvent.cs new file mode 100644 index 000000000000..cf3abecc4470 --- /dev/null +++ b/Maui.Core/Forms/WebNavigationEvent.cs @@ -0,0 +1,10 @@ +namespace System.Maui +{ + public enum WebNavigationEvent + { + Back = 1, + Forward = 2, + NewPage = 3, + Refresh = 4 + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebNavigationEventArgs.cs b/Maui.Core/Forms/WebNavigationEventArgs.cs new file mode 100644 index 000000000000..26a3a6ef6c57 --- /dev/null +++ b/Maui.Core/Forms/WebNavigationEventArgs.cs @@ -0,0 +1,18 @@ +namespace System.Maui +{ + public class WebNavigationEventArgs : EventArgs + { + protected WebNavigationEventArgs(WebNavigationEvent navigationEvent, WebViewSource source, string url) + { + NavigationEvent = navigationEvent; + Source = source; + Url = url; + } + + public WebNavigationEvent NavigationEvent { get; internal set; } + + public WebViewSource Source { get; internal set; } + + public string Url { get; internal set; } + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebNavigationResult.cs b/Maui.Core/Forms/WebNavigationResult.cs new file mode 100644 index 000000000000..55f3cea940dd --- /dev/null +++ b/Maui.Core/Forms/WebNavigationResult.cs @@ -0,0 +1,10 @@ +namespace System.Maui +{ + public enum WebNavigationResult + { + Success = 1, + Cancel = 2, + Timeout = 3, + Failure = 4 + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebViewSource.cs b/Maui.Core/Forms/WebViewSource.cs new file mode 100644 index 000000000000..569eec61dac1 --- /dev/null +++ b/Maui.Core/Forms/WebViewSource.cs @@ -0,0 +1,28 @@ +using System.ComponentModel; + +namespace System.Maui +{ + [TypeConverter(typeof(WebViewSourceTypeConverter))] + public abstract class WebViewSource + { + public static implicit operator WebViewSource(Uri url) + { + return new UrlWebViewSource { Url = url?.AbsoluteUri }; + } + + public static implicit operator WebViewSource(string url) + { + return new UrlWebViewSource { Url = url }; + } + + protected void OnSourceChanged() + { + SourceChanged?.Invoke(this, EventArgs.Empty); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract void Load(IWebViewDelegate renderer); + + internal event EventHandler SourceChanged; + } +} \ No newline at end of file diff --git a/Maui.Core/Forms/WebViewSourceTypeConverter.cs b/Maui.Core/Forms/WebViewSourceTypeConverter.cs new file mode 100644 index 000000000000..303a900874a2 --- /dev/null +++ b/Maui.Core/Forms/WebViewSourceTypeConverter.cs @@ -0,0 +1,14 @@ +namespace System.Maui +{ + [Xaml.TypeConversion(typeof(UrlWebViewSource))] + public class WebViewSourceTypeConverter : TypeConverter + { + public override object ConvertFromInvariantString(string value) + { + if (value != null) + return new UrlWebViewSource { Url = value }; + + throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", value, typeof(UrlWebViewSource))); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Graphics/AffineTransformF.cs b/Maui.Core/Graphics/AffineTransformF.cs new file mode 100644 index 000000000000..35d2911b8fec --- /dev/null +++ b/Maui.Core/Graphics/AffineTransformF.cs @@ -0,0 +1,226 @@ +using System; +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable MemberCanBePrivate.Global + +namespace System.Maui.Graphics { + public class AffineTransformF + { + private const double Zero = 1E-10f; + + private double _m00; + private double _m01; + private double _m02; + private double _m10; + private double _m11; + private double _m12; + + public AffineTransformF() + { + _m00 = _m11 = 1.0f; + _m10 = _m01 = _m02 = _m12 = 0.0f; + } + + public AffineTransformF(AffineTransformF t) + { + _m00 = t._m00; + _m10 = t._m10; + _m01 = t._m01; + _m11 = t._m11; + _m02 = t._m02; + _m12 = t._m12; + } + + public AffineTransformF(double m00, double m10, double m01, double m11, double m02, double m12) + { + _m00 = m00; + _m10 = m10; + _m01 = m01; + _m11 = m11; + _m02 = m02; + _m12 = m12; + } + + public AffineTransformF(double[] matrix) + { + _m00 = matrix[0]; + _m10 = matrix[1]; + _m01 = matrix[2]; + _m11 = matrix[3]; + if (matrix.Length > 4) + { + _m02 = matrix[4]; + _m12 = matrix[5]; + } + } + + public void SetTransform(double m00, double m10, double m01, double m11, double m02, double m12) + { + _m00 = m00; + _m10 = m10; + _m01 = m01; + _m11 = m11; + _m02 = m02; + _m12 = m12; + } + + public void SetTransform(AffineTransformF t) + { + SetTransform(t._m00, t._m10, t._m01, t._m11, t._m02, t._m12); + } + + public void SetToIdentity() + { + _m00 = _m11 = 1.0f; + _m10 = _m01 = _m02 = _m12 = 0.0f; + } + + public void SetToTranslation(double mx, double my) + { + _m00 = _m11 = 1.0f; + _m01 = _m10 = 0.0f; + _m02 = mx; + _m12 = my; + } + + public void SetToScale(double scx, double scy) + { + _m00 = scx; + _m11 = scy; + _m10 = _m01 = _m02 = _m12 = 0.0f; + } + + public void SetToShear(double shx, double shy) + { + _m00 = _m11 = 1.0f; + _m02 = _m12 = 0.0f; + _m01 = shx; + _m10 = shy; + } + + public void SetToRotation(double angle) + { + double sin = (double)Math.Sin(angle); + double cos = (double)Math.Cos(angle); + if (Math.Abs(cos) < Zero) + { + cos = 0.0f; + sin = sin > 0.0f ? 1.0f : -1.0f; + } + else if (Math.Abs(sin) < Zero) + { + sin = 0.0f; + cos = cos > 0.0f ? 1.0f : -1.0f; + } + + _m00 = _m11 = cos; + _m01 = -sin; + _m10 = sin; + _m02 = _m12 = 0.0f; + } + + public void SetToRotation(double angle, double px, double py) + { + SetToRotation(angle); + _m02 = px * (1.0f - _m00) + py * _m10; + _m12 = py * (1.0f - _m00) - px * _m10; + } + + public static AffineTransformF GetTranslateInstance(double mx, double my) + { + var t = new AffineTransformF(); + t.SetToTranslation(mx, my); + return t; + } + + public static AffineTransformF GetScaleInstance(double scx, double scY) + { + var t = new AffineTransformF(); + t.SetToScale(scx, scY); + return t; + } + + public static AffineTransformF GetShearInstance(double shx, double shy) + { + var m = new AffineTransformF(); + m.SetToShear(shx, shy); + return m; + } + + public static AffineTransformF GetRotateInstance(double angle) + { + var t = new AffineTransformF(); + t.SetToRotation(angle); + return t; + } + + public static AffineTransformF GetRotateInstance(double angle, double x, double y) + { + var t = new AffineTransformF(); + t.SetToRotation(angle, x, y); + return t; + } + + public void Translate(double mx, double my) + { + Concatenate(GetTranslateInstance(mx, my)); + } + + public void Scale(double scx, double scy) + { + Concatenate(GetScaleInstance(scx, scy)); + } + + public void Shear(double shx, double shy) + { + Concatenate(GetShearInstance(shx, shy)); + } + + public void RotateInDegrees(double angleInDegrees) + { + Rotate(GraphicsOperations.DegreesToRadians(angleInDegrees)); + } + + public void RotateInDegrees(double angleInDegrees, double px, double py) + { + Rotate(GraphicsOperations.DegreesToRadians(angleInDegrees), px, py); + } + + public void Rotate(double angleInRadians) + { + Concatenate(GetRotateInstance(angleInRadians)); + } + + public void Rotate(double angleInRadians, double px, double py) + { + Concatenate(GetRotateInstance(angleInRadians, px, py)); + } + + private AffineTransformF Multiply(AffineTransformF t1, AffineTransformF t2) + { + return new AffineTransformF( + t1._m00 * t2._m00 + t1._m10 * t2._m01, // m00 + t1._m00 * t2._m10 + t1._m10 * t2._m11, // m01 + t1._m01 * t2._m00 + t1._m11 * t2._m01, // m10 + t1._m01 * t2._m10 + t1._m11 * t2._m11, // m11 + t1._m02 * t2._m00 + t1._m12 * t2._m01 + t2._m02, // m02 + t1._m02 * t2._m10 + t1._m12 * t2._m11 + t2._m12); // m12 + } + + public void Concatenate(AffineTransformF t) + { + SetTransform(Multiply(t, this)); + } + + public Point Transform(Point src) + { + return Transform(src.X, src.Y); + } + + public Point Transform(double x, double y) + { + return new Point(x * _m00 + y * _m01 + _m02, x * _m10 + y * _m11 + _m12); + } + + public bool IsIdentity => _m00 == 1.0f && _m11 == 1.0f && _m10 == 0.0f && _m01 == 0.0f && _m02 == 0.0f && _m12 == 0.0f; + } +} diff --git a/Maui.Core/Graphics/GraphicsOperations.cs b/Maui.Core/Graphics/GraphicsOperations.cs new file mode 100644 index 000000000000..c2024142cc47 --- /dev/null +++ b/Maui.Core/Graphics/GraphicsOperations.cs @@ -0,0 +1,490 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Maui; + +namespace System.Maui.Graphics { + public static class GraphicsOperations + { + public static readonly double Epsilon = double.Epsilon; + + public static Point RotatePoint(Point point, double angleInDegrees) + { + var radians = DegreesToRadians(angleInDegrees); + + var x = (Math.Cos(radians) * point.X - Math.Sin(radians) * point.Y); + var y = (Math.Sin(radians) * point.X + Math.Cos(radians) * point.Y); + + return new Point(x, y); + } + + public static Point RotatePoint(Point center, Point point, double angleInDegrees) + { + var radians = DegreesToRadians(angleInDegrees); + var x = center.X + (double)(Math.Cos(radians) * (point.X - center.X) - Math.Sin(radians) * (point.Y - center.Y)); + var y = center.Y + (double)(Math.Sin(radians) * (point.X - center.X) + Math.Cos(radians) * (point.Y - center.Y)); + return new Point(x, y); + } + + public static float DegreesToRadians(float angleInDegrees) + { + return (float)Math.PI * angleInDegrees / 180; + } + + public static double DegreesToRadians(double angleInDegrees) + { + return Math.PI * angleInDegrees / 180; + } + + public static float RadiansToDegrees (float angleInRadians) + { + return angleInRadians * (180 / (float)Math.PI); + } + + public static double RadiansToDegrees(double angleInRadians) + { + return angleInRadians * (180 / Math.PI); + } + + public static double GetSweep(double angle1, double angle2, bool clockwise) + { + if (clockwise) + { + if (angle2 > angle1) + return angle1 + (360 - angle2); + + return angle1 - angle2; + } + + if (angle1 > angle2) + return angle2 + (360 - angle1); + + return angle2 - angle1; + } + + public static Rectangle GetBoundsOfQuadraticCurve( + Point startPoint, + Point controlPoint, + Point endPoint) + { + return GetBoundsOfQuadraticCurve(startPoint.X, startPoint.Y, controlPoint.X, controlPoint.Y, endPoint.X, endPoint.Y); + } + + public static Rectangle GetBoundsOfQuadraticCurve( + double x0, double y0, + double x1, double y1, + double x2, double y2) + { + var cpx0 = x0 + 2.0f * (x1 - x0) / 3.0f; + var cpy0 = y0 + 2.0f * (y1 - y0) / 3.0f; + var cpx1 = x2 + 2.0f * (x1 - x1) / 3.0f; + var cpy1 = y2 + 2.0f * (y1 - y1) / 3.0f; + + return GetBoundsOfCubicCurve( + x0, y1, + cpx0, cpy0, + cpx1, cpy1, + x2, y2); + } + + public static Rectangle GetBoundsOfCubicCurve( + Point startPoint, + Point controlPoint1, + Point controlPoint2, + Point endPoint) + { + return GetBoundsOfCubicCurve(startPoint.X, startPoint.Y, controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y, endPoint.X, endPoint.Y); + } + + + public static Rectangle GetBoundsOfCubicCurve( + double x0, double y0, + double x1, double y1, + double x2, double y2, + double x3, double y3) + { + var tValues = new List(); + + double t; + + for (var i = 0; i < 2; ++i) + { + double b; + double c; + double a; + + if (i == 0) + { + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + } + else + { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (Math.Abs(a) < Epsilon) + { + if (Math.Abs(b) < Epsilon) + { + continue; + } + + t = -c / b; + if (0 < t && t < 1) + { + tValues.Add(t); + } + + continue; + } + + var b2ac = b * b - 4 * c * a; + var sqrtb2ac = (double)Math.Sqrt(b2ac); + if (b2ac < 0) + { + continue; + } + + var t1 = (-b + sqrtb2ac) / (2 * a); + if (0 < t1 && t1 < 1) + { + tValues.Add(t1); + } + + var t2 = (-b - sqrtb2ac) / (2 * a); + if (0 < t2 && t2 < 1) + { + tValues.Add(t2); + } + } + + var xValues = new List(); + var yValues = new List(); + + for (var j = tValues.Count - 1; j >= 0; j--) + { + t = tValues[j]; + var mt = 1 - t; + var x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + var y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + + xValues.Add(x); + yValues.Add(y); + } + + xValues.Add(x0); + xValues.Add(x3); + yValues.Add(y0); + yValues.Add(y3); + + var minX = xValues.Min(); + var minY = yValues.Min(); + var maxX = xValues.Max(); + var maxY = yValues.Max(); + + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + public static Point GetPointAtAngle(double x, double y, double distance, double radians) + { + var x2 = x + (Math.Cos(radians) * distance); + var y2 = y + (Math.Sin(radians) * distance); + return new Point((double)x2, (double)y2); + } + + public static Point GetPointOnOval( + double x, + double y, + double width, + double height, + double angle) + { + var cx = x + (width / 2); + var cy = y + (height / 2); + + var d = Math.Max(width, height); + var d2 = d / 2; + var fx = width / d; + var fy = height / d; + + while (angle >= 360) + angle -= 360; + + angle *= -1; + + var radians = (double)DegreesToRadians(angle); + var point = GetPointAtAngle(0, 0, d2, radians); + point.X = cx + (point.X * fx); + point.Y = cy + (point.Y * fy); + + return point; + } + + public static double GetAngleAsDegrees(Point point1, Point point2) + { + var dx = point1.X - point2.X; + var dy = point1.Y - point2.Y; + + var radians = (double)Math.Atan2(dy, dx); + var degrees = radians * 180.0f / (double)Math.PI; + + return 180 - degrees; + } + + public static Rectangle GetBoundsOfArc( + double x, + double y, + double width, + double height, + double angle1, + double angle2, + bool clockwise) + { + var x1 = x; + var y1 = y; + var x2 = x + width; + var y2 = y + height; + + var point1 = GetPointOnOval(x, y, width, height, angle1); + var point2 = GetPointOnOval(x, y, width, height, angle2); + var center = new Point(x + width / 2, y + height / 2); + + var startAngle = GetAngleAsDegrees(center, point1); + var endAngle = GetAngleAsDegrees(center, point2); + + var startAngleRadians = DegreesToRadians(startAngle); + var endAngleRadians = DegreesToRadians(endAngle); + + var quadrant1 = GetQuadrant(startAngleRadians); + var quadrant2 = GetQuadrant(endAngleRadians); + + if (quadrant1 == quadrant2) + { + if (clockwise) + { + if (((quadrant1 == 1 || quadrant1 == 2) && (point1.X < point2.X)) || ((quadrant1 == 3 || quadrant1 == 4) && (point1.X > point2.X))) + { + x1 = Math.Min(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + } + else + { + if (((quadrant1 == 1 || quadrant1 == 2) && (point1.X > point2.X)) || ((quadrant1 == 3 || quadrant1 == 4) && (point1.X < point2.X))) + { + x1 = Math.Min(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + } + } + else if (quadrant1 == 1) + { + if (clockwise) + { + if (quadrant2 == 4) + { + x1 = Math.Min(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 3) + { + y1 = Math.Min(point1.Y, point2.Y); + x1 = Math.Min(point1.X, point2.X); + } + else if (quadrant2 == 2) + { + y1 = Math.Min(point1.Y, point2.Y); + } + } + else + { + if (quadrant2 == 2) + { + x1 = Math.Min(point1.X, point2.X); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 3) + { + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 4) + { + x2 = Math.Max(point1.X, point2.X); + } + } + } + else if (quadrant1 == 2) + { + if (clockwise) + { + if (quadrant2 == 1) + { + x1 = Math.Min(point1.X, point2.X); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 4) + { + x1 = Math.Min(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 3) + { + x1 = Math.Min(point1.X, point2.X); + } + } + else + { + if (quadrant2 == 3) + { + y1 = Math.Min(point1.Y, point2.Y); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 4) + { + y1 = Math.Min(point1.Y, point2.Y); + x2 = Math.Max(point1.X, point2.X); + } + else if (quadrant2 == 1) + { + y1 = Math.Min(point1.Y, point2.Y); + } + } + } + else if (quadrant1 == 3) + { + if (clockwise) + { + if (quadrant2 == 2) + { + y1 = Math.Min(point1.Y, point2.Y); + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 1) + { + x2 = Math.Max(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 4) + { + y2 = Math.Max(point1.Y, point2.Y); + } + } + else + { + if (quadrant2 == 4) + { + x1 = Math.Min(point1.X, point2.X); + x2 = Math.Max(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 1) + { + x1 = Math.Min(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 2) + { + x1 = Math.Min(point1.X, point2.X); + } + } + } + else if (quadrant1 == 4) + { + if (clockwise) + { + if (quadrant2 == 3) + { + x1 = Math.Min(point1.X, point2.X); + x2 = Math.Max(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 2) + { + x2 = Math.Max(point1.X, point2.X); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 1) + { + x2 = Math.Max(point1.X, point2.X); + } + } + else + { + if (quadrant2 == 1) + { + x1 = Math.Min(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + y1 = Math.Min(point1.Y, point2.Y); + } + else if (quadrant2 == 2) + { + x1 = Math.Min(point1.X, point2.X); + y2 = Math.Max(point1.Y, point2.Y); + } + else if (quadrant2 == 3) + { + y2 = Math.Max(point1.Y, point2.Y); + } + } + } + + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + public static byte GetQuadrant(double radians) + { + var trueAngle = radians % (2 * Math.PI); + if (trueAngle >= 0.0 && trueAngle < Math.PI / 2.0) + return 1; + if (trueAngle >= Math.PI / 2.0 && trueAngle < Math.PI) + return 2; + if (trueAngle >= Math.PI && trueAngle < Math.PI * 3.0 / 2.0) + return 3; + if (trueAngle >= Math.PI * 3.0 / 2.0 && trueAngle < Math.PI * 2) + return 4; + return 0; + } + + public static Point GetOppositePoint(Point pivot, Point oppositePoint) + { + var dx = oppositePoint.X - pivot.X; + var dy = oppositePoint.Y - pivot.Y; + return new Point(pivot.X - dx, pivot.Y - dy); + } + + public static Point PolarToPoint(double aAngleInRadians, double fx, double fy) + { + var sin = (double)Math.Sin(aAngleInRadians); + var cos = (double)Math.Cos(aAngleInRadians); + return new Point(fx * cos, fy * sin); + } + + public static Point OvalAngleToPoint(double x, double y, double width, double height, double aAngleInDegrees) + { + double vAngle = DegreesToRadians(aAngleInDegrees); + + double cx = x + width / 2; + double cy = y + height / 2; + + Point vPoint = PolarToPoint(vAngle, width / 2, height / 2); + + vPoint.X += cx; + vPoint.Y += cy; + return vPoint; + } + } +} diff --git a/Maui.Core/Graphics/PathBuilder.cs b/Maui.Core/Graphics/PathBuilder.cs new file mode 100644 index 000000000000..07f9743603d8 --- /dev/null +++ b/Maui.Core/Graphics/PathBuilder.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Text; +using System.Globalization; + +namespace System.Maui.Graphics { + public class PathBuilder + { + public static Path Build(string definition) + { + if (string.IsNullOrEmpty(definition)) + return new Path(); + + var pathBuilder = new PathBuilder(); + var path = pathBuilder.BuildPath(definition); + return path; + } + + private readonly Stack _commandStack = new Stack(); + private bool _closeWhenDone; + private char _lastCommand = '~'; + + private Point? _lastCurveControlPoint; + private Point? _relativePoint; + + private Path _path; + + private float NextValue + { + get + { + var valueAsString = _commandStack.Pop(); + return ParseFloat(valueAsString); + } + } + + public static float ParseFloat(string value) + { + if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + return number; + + // Note: Illustrator will sometimes export numbers that look like "5.96.88", so we need to be able to handle them + var split = value.Split('.'); + if (split.Length > 2) + if (float.TryParse($"{split[0]}.{split[1]}", NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return number; + + var stringValue = GetNumbersOnly(value); + if (float.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return number; + + throw new Exception($"Error parsing {value} as a float."); + } + + private static string GetNumbersOnly(string value) + { + var builder = new StringBuilder(value.Length); + foreach (var c in value) + if (char.IsDigit(c) || c == '.' || c == '-') + builder.Append(c); + + return builder.ToString(); + } + + public Path BuildPath(string pathAsString) + { + try + { + _lastCommand = '~'; + _lastCurveControlPoint = null; + _path = null; + _commandStack.Clear(); + _relativePoint = new Point(0, 0); + _closeWhenDone = false; + + pathAsString = pathAsString.Replace("Infinity", "0"); + pathAsString = Regex.Replace(pathAsString, "([a-zA-Z])", " $1 "); + pathAsString = pathAsString.Replace("-", " -"); + pathAsString = pathAsString.Replace(" E -", "E-"); + pathAsString = pathAsString.Replace(" e -", "e-"); + + var args = pathAsString.Split(new[] { ' ', '\r', '\n', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries); + for (var i = args.Length - 1; i >= 0; i--) + { + var entry = args[i]; + var c = entry[0]; + if (char.IsLetter(c)) + { + if (entry.Length > 1) + { + entry = entry.Substring(1); + if (char.IsLetter(entry[0])) + { + if (entry.Length > 1) + { + _commandStack.Push(entry.Substring(1)); + } + + _commandStack.Push(entry[0].ToString(CultureInfo.InvariantCulture)); + } + else + { + _commandStack.Push(entry); + } + } + + _commandStack.Push(c.ToString(CultureInfo.InvariantCulture)); + } + else + { + _commandStack.Push(entry); + } + } + + while (_commandStack.Count > 0) + { + if (_path == null) + { + _path = new Path(); + } + + var vCommand = _commandStack.Pop(); + HandleCommand(vCommand); + } + + if (_path != null && !_path.Closed) + { + if (_closeWhenDone) + { + _path.Close(); + } + } + } + catch (Exception exc) + { + throw new Exception($"An error occurred parsing the path: {pathAsString}", exc); + } + + return _path; + } + + private void HandleCommand(string command) + { + var c = command[0]; + + if (_lastCommand != '~' && (char.IsDigit(c) || c == '-')) + { + if (_lastCommand == 'M') + { + _commandStack.Push(command); + HandleCommand('L'); + } + else if (_lastCommand == 'm') + { + _commandStack.Push(command); + HandleCommand('l'); + } + else if (_lastCommand == 'L') + { + _commandStack.Push(command); + HandleCommand('L'); + } + else if (_lastCommand == 'l') + { + _commandStack.Push(command); + HandleCommand('l'); + } + else if (_lastCommand == 'H') + { + _commandStack.Push(command); + HandleCommand('H'); + } + else if (_lastCommand == 'h') + { + _commandStack.Push(command); + HandleCommand('h'); + } + else if (_lastCommand == 'V') + { + _commandStack.Push(command); + HandleCommand('V'); + } + else if (_lastCommand == 'v') + { + _commandStack.Push(command); + HandleCommand('v'); + } + else if (_lastCommand == 'C') + { + _commandStack.Push(command); + HandleCommand('C'); + } + else if (_lastCommand == 'c') + { + _commandStack.Push(command); + HandleCommand('c'); + } + else if (_lastCommand == 'S') + { + _commandStack.Push(command); + HandleCommand('S'); + } + else if (_lastCommand == 's') + { + _commandStack.Push(command); + HandleCommand('s'); + } + else if (_lastCommand == 'Q') + { + _commandStack.Push(command); + HandleCommand('Q'); + } + else if (_lastCommand == 'q') + { + _commandStack.Push(command); + HandleCommand('q'); + } + else if (_lastCommand == 'T') + { + _commandStack.Push(command); + HandleCommand('T'); + } + else if (_lastCommand == 't') + { + _commandStack.Push(command); + HandleCommand('t'); + } + else if (_lastCommand == 'A') + { + _commandStack.Push(command); + HandleCommand('A'); + } + else if (_lastCommand == 'a') + { + _commandStack.Push(command); + HandleCommand('a'); + } + else + { + Console.WriteLine("Don't know how to handle the path command: " + command); + } + } + else + { + HandleCommand(c); + } + } + + private void HandleCommand(char command) + { + if (command == 'M') + { + MoveTo(false); + } + else if (command == 'm') + { + MoveTo(true); + if (_lastCommand == '~') + { + command = 'm'; + } + } + else if (command == 'z' || command == 'Z') + { + ClosePath(); + } + else if (command == 'L') + { + LineTo(false); + } + else if (command == 'l') + { + LineTo(true); + } + else if (command == 'Q') + { + QuadTo(false); + } + else if (command == 'q') + { + QuadTo(true); + } + else if (command == 'C') + { + CurveTo(false); + } + else if (command == 'c') + { + CurveTo(true); + } + else if (command == 'S') + { + SmoothCurveTo(false); + } + else if (command == 's') + { + SmoothCurveTo(true); + } + else if (command == 'A') + { + ArcTo(false); + } + else if (command == 'a') + { + ArcTo(true); + } + else if (command == 'H') + { + HorizontalLineTo(false); + } + else if (command == 'h') + { + HorizontalLineTo(true); + } + else if (command == 'V') + { + VerticalLineTo(false); + } + else if (command == 'v') + { + VerticalLineTo(true); + } + else + { + Console.WriteLine("Don't know how to handle the path command: " + command); + } + + if (!(command == 'C' || command == 'c' || command == 's' || command == 'S')) + { + _lastCurveControlPoint = null; + } + + _lastCommand = command; + } + + private void ClosePath() + { + _path.Close(); + } + + private void MoveTo(bool isRelative) + { + var point = NewPoint(NextValue, NextValue, isRelative, true); + _path.MoveTo(point); + } + + private void LineTo(bool isRelative) + { + var point = NewPoint(NextValue, NextValue, isRelative, true); + _path.LineTo(point); + } + + private void HorizontalLineTo(bool isRelative) + { + var point = NewHorizontalPoint(NextValue, isRelative, true); + _path.LineTo(point); + } + + private void VerticalLineTo(bool isRelative) + { + var point = NewVerticalPoint(NextValue, isRelative, true); + _path.LineTo(point); + } + + private void CurveTo(bool isRelative) + { + var point = NewPoint(NextValue, NextValue, isRelative, false); + var x = NextValue; + var y = NextValue; + + var isQuad = char.IsLetter(_commandStack.Peek()[0]); + var point2 = NewPoint(x, y, isRelative, isQuad); + + if (isQuad) + { + _path.QuadTo(point, point2); + } + else + { + var point3 = NewPoint(NextValue, NextValue, isRelative, true); + _path.CurveTo(point, point2, point3); + _lastCurveControlPoint = point2; + } + } + + private void QuadTo(bool isRelative) + { + var point1 = NewPoint(NextValue, NextValue, isRelative, false); + var x = NextValue; + var y = NextValue; + + var point2 = NewPoint(x, y, isRelative, true); + _path.QuadTo(point1, point2); + } + + private void SmoothCurveTo(bool isRelative) + { + var point1 = new Point(); + var point2 = NewPoint(NextValue, NextValue, isRelative, false); + + // ReSharper disable ConvertIfStatementToNullCoalescingExpression + if (_relativePoint != null) + { + if (_lastCurveControlPoint == null) + { + // ReSharper restore ConvertIfStatementToNullCoalescingExpression + point1 = GraphicsOperations.GetOppositePoint((Point)_relativePoint, point2); + } + else if (_relativePoint != null) + { + point1 = GraphicsOperations.GetOppositePoint((Point)_relativePoint, (Point)_lastCurveControlPoint); + } + } + + var point3 = NewPoint(NextValue, NextValue, isRelative, true); + _path.CurveTo(point1, point2, point3); + _lastCurveControlPoint = point2; + } + + private void ArcTo(bool isRelative) + { + throw new NotImplementedException(); + } + + private Point NewPoint(float x, float y, bool isRelative, bool isReference) + { + Point point; + + if (isRelative && _relativePoint != null) + { + + point = new Point(((Point)_relativePoint).X + x, ((Point)_relativePoint).Y + y); + } + else + { + point = new Point(x, y); + } + + // If this is the reference point, we want to store the location before + // we translate it into the final coordinates. This way, future relative + // points will start from an un-translated position. + if (isReference) + { + _relativePoint = point; + } + + return point; + } + + private Point NewVerticalPoint(float y, bool isRelative, bool isReference) + { + var point = new Point(); + + if (isRelative && _relativePoint != null) + { + point = new Point(((Point)_relativePoint).X, ((Point)_relativePoint).Y + y); + } + else if (_relativePoint != null) + { + point = new Point(((Point)_relativePoint).X, y); + } + + if (isReference) + _relativePoint = point; + + return point; + } + + private Point NewHorizontalPoint(float x, bool isRelative, bool isReference) + { + var point = new Point(); + + if (isRelative && _relativePoint != null) + { + point = new Point(((Point)_relativePoint).X + x, ((Point)_relativePoint).Y); + } + else if (_relativePoint != null) + { + point = new Point(x, ((Point)_relativePoint).Y); + } + + if (isReference) + _relativePoint = point; + + return point; + } + } +} diff --git a/Maui.Core/Graphics/PathF.cs b/Maui.Core/Graphics/PathF.cs new file mode 100644 index 000000000000..53dacce8f3c4 --- /dev/null +++ b/Maui.Core/Graphics/PathF.cs @@ -0,0 +1,520 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedMethodReturnValue.Global + +namespace System.Maui.Graphics { + public class Path : IDisposable + { + private readonly List _points; + private readonly List _operations; + + private List _arcAngles; + private List _arcClockwise; + + private Rectangle? _cachedBounds; + private object _nativePath; + + public Path (Path prototype, AffineTransformF transform = null) : this() + { + _operations.AddRange(prototype._operations); + foreach (var point in prototype.Points) + { + var newPoint = point; + + if (transform != null) + newPoint = transform.Transform(point); + + _points.Add(newPoint); + } + if (prototype._arcAngles != null) + { + _arcAngles = new List(); + _arcClockwise = new List(); + + _arcAngles.AddRange(prototype._arcAngles); + _arcClockwise.AddRange(prototype._arcClockwise); + } + } + + public Path(Point point) : this() + { + MoveTo(point); + } + + public Path(double x, double y) : this(new Point(x, y)) + { + } + + public Path() + { + _points = new List(); + _operations = new List(); + } + + public bool Closed + { + get + { + if (_operations.Count > 0) + return _operations[_operations.Count - 1] == PathOperation.Close; + + return false; + } + } + + public Point? FirstPoint + { + get + { + if (_points != null && _points.Count > 0) + return _points[0]; + + return null; + } + } + + public IEnumerable PathOperations + { + get + { + for (var i = 0; i < _operations.Count; i++) + yield return _operations[i]; + } + } + + public IEnumerable Points + { + get + { + for (var i = 0; i < _points.Count; i++) + yield return _points[i]; + } + } + + public Rectangle Bounds + { + get + { + if (_cachedBounds != null) + return (Rectangle)_cachedBounds; + + _cachedBounds = CalculateBounds(); + + /* var graphicsService = Device.GraphicsService; + if (graphicsService != null) + _cachedBounds = graphicsService.GetPathBounds(this); + else + { + + }*/ + + return (Rectangle)_cachedBounds; + } + } + + private Rectangle CalculateBounds() + { + var xValues = new List(); + var yValues = new List(); + + int pointIndex = 0; + int arcAngleIndex = 0; + int arcClockwiseIndex = 0; + + foreach (var operation in PathOperations) + { + if (operation == PathOperation.MoveTo) + { + pointIndex++; + } + else if (operation == PathOperation.Line) + { + var startPoint = _points[pointIndex - 1]; + var endPoint = _points[pointIndex++]; + + xValues.Add(startPoint.X); + xValues.Add(endPoint.X); + yValues.Add(startPoint.Y); + yValues.Add(endPoint.Y); + } + else if (operation == PathOperation.Quad) + { + var startPoint = _points[pointIndex - 1]; + var controlPoint = _points[pointIndex++]; + var endPoint = _points[pointIndex++]; + + var bounds = GraphicsOperations.GetBoundsOfQuadraticCurve(startPoint, controlPoint, endPoint); + + xValues.Add(bounds.Left); + xValues.Add(bounds.Right); + yValues.Add(bounds.Top); + yValues.Add(bounds.Bottom); + } + else if (operation == PathOperation.Cubic) + { + var startPoint = _points[pointIndex - 1]; + var controlPoint1 = _points[pointIndex++]; + var controlPoint2 = _points[pointIndex++]; + var endPoint = _points[pointIndex++]; + + var bounds = GraphicsOperations.GetBoundsOfCubicCurve(startPoint, controlPoint1, controlPoint2, endPoint); + + xValues.Add(bounds.Left); + xValues.Add(bounds.Right); + yValues.Add(bounds.Top); + yValues.Add(bounds.Bottom); + } + else if (operation == PathOperation.Arc) + { + var topLeft = _points[pointIndex++]; + var bottomRight = _points[pointIndex++]; + double startAngle = GetArcAngle(arcAngleIndex++); + double endAngle = GetArcAngle(arcAngleIndex++); + var clockwise = IsArcClockwise(arcClockwiseIndex++); + + var bounds = GraphicsOperations.GetBoundsOfArc(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y, startAngle, endAngle, clockwise); + + xValues.Add(bounds.Left); + xValues.Add(bounds.Right); + yValues.Add(bounds.Top); + yValues.Add(bounds.Bottom); + } + } + + var minX = xValues.Min(); + var minY = yValues.Min(); + var maxX = xValues.Max(); + var maxY = yValues.Max(); + + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + public Point? LastPoint + { + get + { + if (_points != null && _points.Count > 0) + return _points[_points.Count - 1]; + + return null; + } + } + + public Point this[int index] + { + get + { + if (index < 0 || index >= _points.Count) + throw new IndexOutOfRangeException(); + + return _points[index]; + } + } + + public int PointCount => _points.Count; + + public int OperationCount => _operations.Count; + + public PathOperation GetOperationType(int index) + { + return _operations[index]; + } + + public double GetArcAngle(int index) + { + if (_arcAngles != null && _arcAngles.Count > index) + return _arcAngles[index]; + + return 0; + } + + public bool IsArcClockwise(int index) + { + if (_arcClockwise != null && _arcClockwise.Count > index) + return _arcClockwise[index]; + + return false; + } + + public Path MoveTo(double x, double y) + { + return MoveTo(new Point(x, y)); + } + + public Path MoveTo(Point point) + { + _points.Add(point); + _operations.Add(PathOperation.MoveTo); + Invalidate(); + return this; + } + + public void Close() + { + if (!Closed) + _operations.Add(PathOperation.Close); + + Invalidate(); + } + + public Path LineTo(double x, double y) + { + return LineTo(new Point(x, y)); + } + + public Path LineTo(Point point) + { + if (_points.Count == 0) + { + _points.Add(point); + _operations.Add(PathOperation.MoveTo); + } + else + { + _points.Add(point); + _operations.Add(PathOperation.Line); + } + + Invalidate(); + + return this; + } + + public Path AddArc(double x1, double y1, double x2, double y2, double startAngle, double endAngle, bool clockwise) + { + return AddArc(new Point(x1, y1), new Point(x2, y2), startAngle, endAngle, clockwise); + } + + public Path AddArc(Point topLeft, Point bottomRight, double startAngle, double endAngle, bool clockwise) + { + if (_arcAngles == null) + { + _arcAngles = new List(); + _arcClockwise = new List(); + } + _points.Add(topLeft); + _points.Add(bottomRight); + _arcAngles.Add(startAngle); + _arcAngles.Add(endAngle); + _arcClockwise.Add(clockwise); + _operations.Add(PathOperation.Arc); + Invalidate(); + return this; + } + + public Path QuadTo(double cx, double cy, double x, double y) + { + return QuadTo(new Point(cx, cy), new Point(x, y)); + } + + public Path QuadTo(Point controlPoint, Point point) + { + _points.Add(controlPoint); + _points.Add(point); + _operations.Add(PathOperation.Quad); + Invalidate(); + return this; + } + + public Path CurveTo(double c1X, double c1Y, double c2X, double c2Y, double x, double y) + { + return CurveTo(new Point(c1X, c1Y), new Point(c2X, c2Y), new Point(x, y)); + } + + public Path CurveTo(Point controlPoint1, Point controlPoint2, Point point) + { + _points.Add(controlPoint1); + _points.Add(controlPoint2); + _points.Add(point); + _operations.Add(PathOperation.Cubic); + Invalidate(); + return this; + } + + public Path Rotate(double angle) + { + var center = Bounds.Center; + return Rotate(angle, center); + } + + public Path Rotate(double angle, Point pivotPoint) + { + var path = new Path(); + + var index = 0; + var arcIndex = 0; + var clockwiseIndex = 0; + + foreach (var operation in _operations) + { + if (operation == PathOperation.MoveTo) + { + var point = GetRotatedPoint(index++, pivotPoint, angle); + path.MoveTo(point); + } + else if (operation == PathOperation.Line) + { + var point = GetRotatedPoint(index++, pivotPoint, angle); + path.LineTo(point.X, point.Y); + } + else if (operation == PathOperation.Quad) + { + var controlPoint = GetRotatedPoint(index++, pivotPoint, angle); + var point = GetRotatedPoint(index++, pivotPoint, angle); + path.QuadTo(controlPoint.X, controlPoint.Y, point.X, point.Y); + } + else if (operation == PathOperation.Cubic) + { + var controlPoint1 = GetRotatedPoint(index++, pivotPoint, angle); + var controlPoint2 = GetRotatedPoint(index++, pivotPoint, angle); + var point = GetRotatedPoint(index++, pivotPoint, angle); + path.CurveTo(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y, point.X, point.Y); + } + else if (operation == PathOperation.Arc) + { + var topLeft = GetRotatedPoint(index++, pivotPoint, angle); + var bottomRight = GetRotatedPoint(index++, pivotPoint, angle); + var startAngle = _arcAngles[arcIndex++]; + var endAngle = _arcAngles[arcIndex++]; + var clockwise = _arcClockwise[clockwiseIndex++]; + + path.AddArc(topLeft, bottomRight, startAngle, endAngle, clockwise); + } + else if (operation == PathOperation.Close) + { + path.Close(); + } + } + + return path; + } + + private Point GetRotatedPoint(int index, Point center, double angleInDegrees) + { + var point = _points[index]; + return GraphicsOperations.RotatePoint(center, point, angleInDegrees); + } + + public void AppendEllipse(Rectangle rect) + { + AppendEllipse(rect.X, rect.Y, rect.Width, rect.Height); + } + + public void AppendEllipse(double x, double y, double w, double h) + { + var minx = x; + var miny = y; + var maxx = minx + w; + var maxy = miny + h; + var midx = minx + (w / 2); + var midy = miny + (h / 2); + var offsetY = h / 2 * .55f; + var offsetX = w / 2 * .55f; + + MoveTo(new Point(minx, midy)); + CurveTo(new Point(minx, midy - offsetY), new Point(midx - offsetX, miny), new Point(midx, miny)); + CurveTo(new Point(midx + offsetX, miny), new Point(maxx, midy - offsetY), new Point(maxx, midy)); + CurveTo(new Point(maxx, midy + offsetY), new Point(midx + offsetX, maxy), new Point(midx, maxy)); + CurveTo(new Point(midx - offsetX, maxy), new Point(minx, midy + offsetY), new Point(minx, midy)); + Close(); + } + + public void AppendRectangle(Rectangle rect, bool includeLast = false) + { + AppendRectangle(rect.X, rect.Y, rect.Width, rect.Height, includeLast); + } + + public void AppendRectangle(double x, double y, double w, double h, bool includeLast = false) + { + var minx = x; + var miny = y; + var maxx = minx + w; + var maxy = miny + h; + + MoveTo(new Point(minx, miny)); + LineTo(new Point(maxx, miny)); + LineTo(new Point(maxx, maxy)); + LineTo(new Point(minx, maxy)); + + if (includeLast) + LineTo(new Point(minx, miny)); + + Close(); + } + + public void AppendRoundedRectangle(Rectangle rect, double cornerRadius, bool includeLast = false) + { + AppendRoundedRectangle(rect.X, rect.Y, rect.Width, rect.Height, cornerRadius, includeLast); + } + + public void AppendRoundedRectangle(double x, double y, double w, double h, double cornerRadius, bool includeLast = false) + { + if (cornerRadius > h / 2) + cornerRadius = h / 2; + + if (cornerRadius > w / 2) + cornerRadius = w / 2; + + var minx = x; + var miny = y; + var maxx = minx + w; + var maxy = miny + h; + + var handleOffset = cornerRadius * .55f; + var cornerOffset = cornerRadius - handleOffset; + + MoveTo(new Point(minx, miny + cornerRadius)); + CurveTo(new Point(minx, miny + cornerOffset), new Point(minx + cornerOffset, miny), new Point(minx + cornerRadius, miny)); + LineTo(new Point(maxx - cornerRadius, miny)); + CurveTo(new Point(maxx - cornerOffset, miny), new Point(maxx, miny + cornerOffset), new Point(maxx, miny + cornerRadius)); + LineTo(new Point(maxx, maxy - cornerRadius)); + CurveTo(new Point(maxx, maxy - cornerOffset), new Point(maxx - cornerOffset, maxy), new Point(maxx - cornerRadius, maxy)); + LineTo(new Point(minx + cornerRadius, maxy)); + CurveTo(new Point(minx + cornerOffset, maxy), new Point(minx, maxy - cornerOffset), new Point(minx, maxy - cornerRadius)); + + if (includeLast) + LineTo(new Point(minx, miny + cornerRadius)); + + Close(); + } + + public object NativePath + { + get => _nativePath; + set + { + if (_nativePath is IDisposable disposable) + disposable.Dispose(); + + _nativePath = value; + } + } + + private void Invalidate() + { + _cachedBounds = null; + ReleaseNative(); + } + + public void Dispose() + { + ReleaseNative(); + } + + private void ReleaseNative() + { + if (_nativePath is IDisposable disposable) + disposable.Dispose(); + + _nativePath = null; + } + + public Path Transform(AffineTransformF transform) + { + return new Path(this, transform); + } + } +} diff --git a/Maui.Core/Graphics/PathOperation.cs b/Maui.Core/Graphics/PathOperation.cs new file mode 100644 index 000000000000..6c57ffad3b5a --- /dev/null +++ b/Maui.Core/Graphics/PathOperation.cs @@ -0,0 +1,11 @@ +namespace System.Maui.Graphics { + public enum PathOperation + { + MoveTo, + Line, + Quad, + Cubic, + Arc, + Close + } +} diff --git a/Maui.Core/Graphics/PathScaling.cs b/Maui.Core/Graphics/PathScaling.cs new file mode 100644 index 000000000000..e8ef5dcce9a3 --- /dev/null +++ b/Maui.Core/Graphics/PathScaling.cs @@ -0,0 +1,10 @@ +using System; +namespace System.Maui.Graphics { + public enum PathScaling + { + AspectFit, + AspectFill, + Fill, + None + } +} diff --git a/Maui.Core/IApp.cs b/Maui.Core/IApp.cs new file mode 100644 index 000000000000..51ef5946c404 --- /dev/null +++ b/Maui.Core/IApp.cs @@ -0,0 +1,5 @@ +using System; +namespace System.Maui.Core { + public interface IApp { + } +} diff --git a/Maui.Core/Internals/ReflectionExtensions.cs b/Maui.Core/Internals/ReflectionExtensions.cs new file mode 100644 index 000000000000..1d2c9c555118 --- /dev/null +++ b/Maui.Core/Internals/ReflectionExtensions.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace System.Maui.Internals +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class ReflectionExtensions + { + public static FieldInfo GetField(this Type type, Func predicate) + { + return GetFields(type).FirstOrDefault(predicate); + } + + public static FieldInfo GetField(this Type type, string name) + { + return type.GetField(fi => fi.Name == name); + } + + public static IEnumerable GetFields(this Type type) + { + return GetParts(type, i => i.DeclaredFields); + } + + public static IEnumerable GetProperties(this Type type) + { + return GetParts(type, ti => ti.DeclaredProperties); + } + + public static PropertyInfo GetProperty(this Type type, string name) + { + Type t = type; + while (t != null) + { + TypeInfo ti = t.GetTypeInfo(); + PropertyInfo property = ti.GetDeclaredProperty(name); + if (property != null) + return property; + + t = ti.BaseType; + } + + return null; + } + + public static object[] GetCustomAttributesSafe(this Assembly assembly, Type attrType) + { + try + { +#if NETSTANDARD2_0 + return assembly.GetCustomAttributes(attrType, true); +#else + return assembly.GetCustomAttributes(attrType).ToArray(); +#endif + } + catch (System.IO.FileNotFoundException) + { + // Sometimes the previewer doesn't actually have everything required for these loads to work + //TODO: figure this out + //Log.Warning(nameof(Registrar), "Could not load assembly: {0} for Attribute {1} | Some renderers may not be loaded", assembly.FullName, attrType.FullName); + } + + return null; + } + + public static Type[] GetExportedTypes(this Assembly assembly) + { + return assembly.ExportedTypes.ToArray(); + } + + public static bool IsAssignableFrom(this Type self, Type c) + { + return self.GetTypeInfo().IsAssignableFrom(c.GetTypeInfo()); + } + + public static bool IsInstanceOfType(this Type self, object o) + { + return self.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); + } + + static IEnumerable GetParts(Type type, Func> selector) + { + Type t = type; + while (t != null) + { + TypeInfo ti = t.GetTypeInfo(); + foreach (T f in selector(ti)) + yield return f; + t = ti.BaseType; + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Internals/Ticker.Android.cs b/Maui.Core/Internals/Ticker.Android.cs new file mode 100644 index 000000000000..d4467516c13a --- /dev/null +++ b/Maui.Core/Internals/Ticker.Android.cs @@ -0,0 +1,111 @@ +using System.Maui.Platform; +using Android.Animation; +using Android.Content; +using Android.OS; + +namespace System.Maui.Internals +{ + internal class NativeTicker : Ticker, IDisposable + { + ValueAnimator _val; + bool _energySaveModeDisabled; + readonly bool _animatorEnabled; + + public NativeTicker() + { + _val = new ValueAnimator(); + _val.SetIntValues(0, 100); // avoid crash + _val.RepeatCount = ValueAnimator.Infinite; + _val.Update += OnValOnUpdate; + _animatorEnabled = IsAnimatorEnabled(); + CheckPowerSaveModeStatus(); + } + + public override bool SystemEnabled => _energySaveModeDisabled && _animatorEnabled; + + internal void CheckPowerSaveModeStatus() + { + // Android disables animations when it's in power save mode + // So we need to keep track of whether we're in that mode and handle animations accordingly + // We can't just check ValueAnimator.AreAnimationsEnabled() because there's no event for that, and it's + // only supported on API >= 26 + + if (!NativeVersion.Supports(NativeApis.PowerSaveMode)) + { + _energySaveModeDisabled = true; + return; + } + + // TODO Figure out what Forms.ApplicationContext will look like in this brave new world + //var powerManager = (PowerManager)Forms.ApplicationContext.GetSystemService(Context.PowerService); + + //var powerSaveOn = powerManager.IsPowerSaveMode; + + //// If power saver is active, then animations will not run + //_energySaveModeDisabled = !powerSaveOn; + + //// Notify the ticker that this value has changed, so it can manage animations in progress + //OnSystemEnabledChanged(); + } + + static bool IsAnimatorEnabled() + { + var resolver = global::Android.App.Application.Context?.ContentResolver; + if (resolver == null) + { + return false; + } + + float animationScale; + + if (Build.VERSION.SdkInt >= BuildVersionCodes.JellyBeanMr1) + { + animationScale = global::Android.Provider.Settings.Global.GetFloat(resolver, global::Android.Provider.Settings.Global.AnimatorDurationScale, 1); + } + else + { +#pragma warning disable 0618 + animationScale = global::Android.Provider.Settings.System.GetFloat(resolver, global::Android.Provider.Settings.System.AnimatorDurationScale, 1); +#pragma warning restore 0618 + } + + return animationScale > 0; + } + + public void Dispose() + { + if (_val != null) + { + _val.Update -= OnValOnUpdate; + _val.Dispose(); + } + _val = null; + } + + protected override void DisableTimer() + { + //TODO:Bring back + //if (Device.IsInvokeRequired) + //{ + // Device.BeginInvokeOnMainThread(new Action(() => + // { + // _val?.Cancel(); + // })); + //} + //else + { + _val?.Cancel(); + } + } + + protected override void EnableTimer() + { + _val?.Start(); + } + + void OnValOnUpdate(object sender, ValueAnimator.AnimatorUpdateEventArgs e) + { + SendSignals(); + } + } +} diff --git a/Maui.Core/Internals/Ticker.Mac.cs b/Maui.Core/Internals/Ticker.Mac.cs new file mode 100644 index 000000000000..ea8c6d051663 --- /dev/null +++ b/Maui.Core/Internals/Ticker.Mac.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using Foundation; +using CoreVideo; +using AppKit; +using CoreAnimation; +using System.Maui.Internals; + +namespace System.Maui.Internals +{ + // ReSharper disable once InconsistentNaming + internal class NativeTicker : Ticker + { + readonly BlockingCollection _queue = new BlockingCollection(); + CVDisplayLink _link; + + public NativeTicker() + { + var thread = new Thread(StartThread); + thread.Start(); + } + + internal new static NativeTicker Default => Ticker.Default as NativeTicker; + + public void Invoke(Action action) + { + _queue.Add(action); + } + + protected override void DisableTimer() + { + _link?.Stop(); + _link?.Dispose(); + _link = null; + } + + protected override void EnableTimer() + { + _link = new CVDisplayLink(); + _link.SetOutputCallback(DisplayLinkOutputCallback); + _link.Start(); + } + + public CVReturn DisplayLinkOutputCallback(CVDisplayLink displayLink, ref CVTimeStamp inNow, + ref CVTimeStamp inOutputTime, CVOptionFlags flagsIn, ref CVOptionFlags flagsOut) + { + // There is no autorelease pool when this method is called because it will be called from a background thread + // It's important to create one or you will leak objects + // ReSharper disable once UnusedVariable + //TODO: Bring this back! + //using (var pool = new NSAutoreleasePool()) + //{ + // Device.BeginInvokeOnMainThread(() => SendSignals()); + //} + SendSignals(); + return CVReturn.Success; + } + + void StartThread() + { + while (true) + { + Action action = _queue.Take(); + bool previous = NSApplication.CheckForIllegalCrossThreadCalls; + NSApplication.CheckForIllegalCrossThreadCalls = false; + + CATransaction.Begin(); + action.Invoke(); + + while (_queue.TryTake(out action)) + action.Invoke(); + CATransaction.Commit(); + + NSApplication.CheckForIllegalCrossThreadCalls = previous; + } + // ReSharper disable once FunctionNeverReturns + } + } +} \ No newline at end of file diff --git a/Maui.Core/Internals/Ticker.Standard.cs b/Maui.Core/Internals/Ticker.Standard.cs new file mode 100644 index 000000000000..b74b2d263194 --- /dev/null +++ b/Maui.Core/Internals/Ticker.Standard.cs @@ -0,0 +1,40 @@ +using System.Timers; + +namespace System.Maui.Internals +{ + internal partial class NativeTicker : Ticker + { + void Timer_Elapsed(object sender, ElapsedEventArgs e) => SendSignals(); + + Timer timer; + + public virtual int MaxFps { get; set; } = 60; + + protected override void DisableTimer() + { + if (timer == null) + return; + timer.AutoReset = false; + timer.Stop(); + timer.Elapsed -= Timer_Elapsed; + timer.Dispose(); + timer = null; + } + + protected override void EnableTimer() + { + if (timer != null) + { + return; + } + timer = new Timer + { + AutoReset = true, + Interval = 1000 / MaxFps, + }; + timer.Elapsed += Timer_Elapsed; + timer.AutoReset = true; + timer.Start(); + } + } +} diff --git a/Maui.Core/Internals/Ticker.Win32.cs b/Maui.Core/Internals/Ticker.Win32.cs new file mode 100644 index 000000000000..b74b2d263194 --- /dev/null +++ b/Maui.Core/Internals/Ticker.Win32.cs @@ -0,0 +1,40 @@ +using System.Timers; + +namespace System.Maui.Internals +{ + internal partial class NativeTicker : Ticker + { + void Timer_Elapsed(object sender, ElapsedEventArgs e) => SendSignals(); + + Timer timer; + + public virtual int MaxFps { get; set; } = 60; + + protected override void DisableTimer() + { + if (timer == null) + return; + timer.AutoReset = false; + timer.Stop(); + timer.Elapsed -= Timer_Elapsed; + timer.Dispose(); + timer = null; + } + + protected override void EnableTimer() + { + if (timer != null) + { + return; + } + timer = new Timer + { + AutoReset = true, + Interval = 1000 / MaxFps, + }; + timer.Elapsed += Timer_Elapsed; + timer.AutoReset = true; + timer.Start(); + } + } +} diff --git a/Maui.Core/Internals/Ticker.cs b/Maui.Core/Internals/Ticker.cs new file mode 100644 index 000000000000..66a5778b63c4 --- /dev/null +++ b/Maui.Core/Internals/Ticker.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Timers; +using System.Maui.Platform; + +namespace System.Maui.Internals +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract partial class Ticker + { + static Ticker s_ticker; + readonly Stopwatch _stopwatch; + readonly List>> _timeouts; + + int _count; + bool _enabled; + + protected Ticker() + { + _count = 0; + _timeouts = new List>>(); + + _stopwatch = new Stopwatch(); + } + + // Some devices may suspend the services we use for the ticker (e.g., in power save mode) + // The native implementations can override this value as needed + public virtual bool SystemEnabled => true; + + // Native ticker implementations can let us know that the ticker has been enabled/disabled by the system + protected void OnSystemEnabledChanged() + { + if (!SystemEnabled) + { + // Something (possibly power save mode) has disabled the ticker; tell all the current in-progress + // timeouts to finish + SendFinish(); + } + } + + public static void SetDefault(Ticker ticker) => Default = ticker; + public static Ticker Default + { + internal set { + if (value == null && s_ticker != null) + { + (s_ticker as IDisposable)?.Dispose(); + } + s_ticker = value; + } + get + { + s_ticker ??= new NativeTicker(); + return s_ticker.GetTickerInstance(); + } + } + + protected virtual Ticker GetTickerInstance() + { + // This method is provided so platforms can override it and return something other than + // the normal Ticker singleton + return s_ticker; + } + + public virtual int Insert(Func timeout) + { + _count++; + _timeouts.Add(new Tuple>(_count, timeout)); + + if (!_enabled) + { + _enabled = true; + Enable(); + } + + return _count; + } + + public virtual void Remove(int handle) + { + //Device.BeginInvokeOnMainThread(() => + //{ + // RemoveTimeout(handle); + //}); + } + + public virtual void Remove(int handle, IDispatcher dispatcher) + { + dispatcher.BeginInvokeOnMainThread(() => + { + RemoveTimeout(handle); + }); + } + + void RemoveTimeout(int handle) + { + _timeouts.RemoveAll(t => t.Item1 == handle); + + if (_timeouts.Count == 0) + { + _enabled = false; + Disable(); + } + } + + protected abstract void DisableTimer(); + + protected abstract void EnableTimer(); + + protected void SendFinish() + { + SendSignals(long.MaxValue); + } + + protected void SendSignals(int timestep = -1) + { + long step = timestep >= 0 + ? timestep + : _stopwatch.ElapsedMilliseconds; + + SendSignals(step); + } + + protected void SendSignals(long step) + { + _stopwatch.Reset(); + _stopwatch.Start(); + + var localCopy = new List>>(_timeouts); + foreach (Tuple> timeout in localCopy) + { + bool remove = !timeout.Item2(step); + if (remove) + _timeouts.RemoveAll(t => t.Item1 == timeout.Item1); + } + + if (_timeouts.Count == 0) + { + _enabled = false; + Disable(); + } + } + + void Disable() + { + _stopwatch.Reset(); + DisableTimer(); + } + + void Enable() + { + _stopwatch.Start(); + EnableTimer(); + } + } +} diff --git a/Maui.Core/Internals/Ticker.iOS.cs b/Maui.Core/Internals/Ticker.iOS.cs new file mode 100644 index 000000000000..a4daa95c4608 --- /dev/null +++ b/Maui.Core/Internals/Ticker.iOS.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Concurrent; +using System.Maui.Internals; +using System.Threading; +using CoreAnimation; +using Foundation; +using UIKit; + +namespace System.Maui.Internals +{ + internal class NativeTicker : Ticker + { + readonly BlockingCollection _queue = new BlockingCollection(); + CADisplayLink _link; + + public NativeTicker() + { + var thread = new Thread(StartThread); + thread.Start(); + } + + internal new static NativeTicker Default + { + get { return Ticker.Default as NativeTicker; } + } + + public void Invoke(Action action) + { + _queue.Add(action); + } + + protected override void DisableTimer() + { + if (_link != null) + { + _link.RemoveFromRunLoop(NSRunLoop.Current, NSRunLoop.NSRunLoopCommonModes); + _link.Dispose(); + } + _link = null; + } + + protected override void EnableTimer() + { + _link = CADisplayLink.Create(() => SendSignals()); + _link.AddToRunLoop(NSRunLoop.Current, NSRunLoop.NSRunLoopCommonModes); + } + + void StartThread() + { + while (true) + { + var action = _queue.Take(); + var previous = UIApplication.CheckForIllegalCrossThreadCalls; + UIApplication.CheckForIllegalCrossThreadCalls = false; + + CATransaction.Begin(); + action.Invoke(); + + while (_queue.TryTake(out action)) + action.Invoke(); + CATransaction.Commit(); + + UIApplication.CheckForIllegalCrossThreadCalls = previous; + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Internals/Tweener.cs b/Maui.Core/Internals/Tweener.cs new file mode 100644 index 000000000000..45a0f81b516d --- /dev/null +++ b/Maui.Core/Internals/Tweener.cs @@ -0,0 +1,117 @@ +namespace System.Maui.Internals +{ + internal class Tweener + { + long _lastMilliseconds; + + int _timer; + + public Tweener(uint length) + { + Value = 0.0f; + Length = length; + Loop = false; + } + + public AnimatableKey Handle { get; set; } + + public uint Length { get; } + + public bool Loop { get; set; } + + public double Value { get; private set; } + + public event EventHandler Finished; + + public void Pause() + { + if (_timer != 0) + { + Ticker.Default.Remove(_timer); + _timer = 0; + } + } + + public void Start() + { + Pause(); + + _lastMilliseconds = 0; + + if (!Ticker.Default.SystemEnabled) + { + FinishImmediately(); + return; + } + + _timer = Ticker.Default.Insert(step => + { + if (step == long.MaxValue) + { + // We're being forced to finish + Value = 1.0; + } + else + { + long ms = step + _lastMilliseconds; + + Value = Math.Min(1.0f, ms / (double)Length); + + _lastMilliseconds = ms; + } + + ValueUpdated?.Invoke(this, EventArgs.Empty); + + if (Value >= 1.0f) + { + if (Loop) + { + _lastMilliseconds = 0; + Value = 0.0f; + return true; + } + + Finished?.Invoke(this, EventArgs.Empty); + Value = 0.0f; + _timer = 0; + return false; + } + return true; + }); + } + + void FinishImmediately() + { + Value = 1.0f; + ValueUpdated?.Invoke(this, EventArgs.Empty); + Finished?.Invoke(this, EventArgs.Empty); + Value = 0.0f; + _timer = 0; + } + + public void Stop() + { + Pause(); + Value = 1.0f; + Finished?.Invoke(this, EventArgs.Empty); + Value = 0.0f; + } + + public event EventHandler ValueUpdated; + + ~Tweener() + { + if (_timer != 0) + { + try + { + Ticker.Default.Remove(_timer); + } + catch (InvalidOperationException) + { + } + } + _timer = 0; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Layout/Stack.cs b/Maui.Core/Layout/Stack.cs new file mode 100644 index 000000000000..c9f68f7dadfd --- /dev/null +++ b/Maui.Core/Layout/Stack.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; + +namespace System.Maui.Core.Layout +{ + public static class Stack + { + public static SizeRequest MeasureStack(double widthConstraint, double heightConstraint, IEnumerable views, Orientation orientation) + { + if (orientation == Orientation.Horizontal) + { + return MeasureStackHorizontal(heightConstraint, views); + } + + return MeasureStackVertical(widthConstraint, views); + } + + static SizeRequest MeasureStackVertical(double constraint, IEnumerable views) + { + double totalRequestedHeight = 0; + double totalMinimumHeight = 0; + double requestedWidth = 0; + double minimumWidth = 0; + + foreach (var child in views) + { + // TODO check child.IsVisible + + var measure = child.IsMeasureValid ? child.DesiredSize : child.Measure(constraint, double.PositiveInfinity); + totalRequestedHeight += measure.Request.Height; + totalMinimumHeight += measure.Minimum.Height; + + requestedWidth = Math.Max(requestedWidth, measure.Request.Width); + minimumWidth = Math.Max(minimumWidth, measure.Minimum.Width); + } + + return new SizeRequest( + new Size(requestedWidth, totalRequestedHeight), + new Size(minimumWidth, totalMinimumHeight)); + } + + static SizeRequest MeasureStackHorizontal(double constraint, IEnumerable views) + { + double totalRequestedWidth = 0; + double totalMinimumWidth = 0; + double requestedHeight = 0; + double minimumHeight = 0; + + foreach (var child in views) + { + // TODO check child.IsVisible + + var measure = child.IsMeasureValid ? child.DesiredSize : child.Measure(double.PositiveInfinity, constraint); + totalRequestedWidth += measure.Request.Width; + totalMinimumWidth += measure.Minimum.Width; + + requestedHeight = Math.Max(requestedHeight, measure.Request.Height); + minimumHeight = Math.Max(minimumHeight, measure.Minimum.Height); + } + + return new SizeRequest( + new Size(totalRequestedWidth, requestedHeight), + new Size(totalMinimumWidth, minimumHeight)); + } + + public static void ArrangeStack(Rectangle bounds, IEnumerable views, Orientation orientation) + { + switch (orientation) + { + case Orientation.Vertical: + ArrangeStackVertically(bounds.Width, views); + break; + case Orientation.Horizontal: + ArrangeStackHorizontally(bounds.Height, views); + break; + } + } + + static void ArrangeStackHorizontally(double heightConstraint, IEnumerable views) + { + double stackWidth = 0; + + foreach (var child in views) + { + var destination = new Rectangle(stackWidth, 0, child.DesiredSize.Request.Width, heightConstraint); + child.Arrange(destination); + + stackWidth += destination.Width; + } + } + + static void ArrangeStackVertically(double widthConstraint, IEnumerable views) + { + double stackHeight = 0; + + foreach (var child in views) + { + var destination = new Rectangle(0, stackHeight, widthConstraint, child.DesiredSize.Request.Height); + child.Arrange(destination); + + stackHeight += destination.Height; + } + } + } +} + + +// TODO Android GetDesiredSize needs to convert double.Infinity (or maxvalue?) to measurespec unspecified (rather than atmost) \ No newline at end of file diff --git a/Maui.Core/Maui.Core.csproj b/Maui.Core/Maui.Core.csproj new file mode 100644 index 000000000000..19af62503dba --- /dev/null +++ b/Maui.Core/Maui.Core.csproj @@ -0,0 +1,64 @@ + + + $(TargetFrameworks);netstandard2.1;Xamarin.iOS10;MonoAndroid10.0;Xamarin.Mac20;netcoreapp3.1 + $(TargetFrameworks);netstandard2.1;Xamarin.iOS10;MonoAndroid10.0;Xamarin.Mac20 + System.Maui + + + 8.0 + System.Maui + Maui + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Maui.Core/Platform/Android/ButtonLayoutManager.Android.cs b/Maui.Core/Platform/Android/ButtonLayoutManager.Android.cs new file mode 100644 index 000000000000..934dcc750945 --- /dev/null +++ b/Maui.Core/Platform/Android/ButtonLayoutManager.Android.cs @@ -0,0 +1,347 @@ +//using System; +//using System.ComponentModel; +//using Android.Content; +//using Android.Graphics; +//using Android.Graphics.Drawables; +//#if __ANDROID_29__ +//using AndroidX.Core.View; +//using AndroidX.Core.Widget; +//using AndroidX.AppCompat.Widget; +//#else +//using Android.Support.V4.View; +//using Android.Support.V4.Widget; +//using Android.Support.V7.Widget; +//#endif +//using System.Maui.Core.Internals; +//using AView = Android.Views.View; + +//namespace System.Maui.Platform.Android +//{ +// // TODO: Currently the drawable is reloaded if the text or the layout changes. +// // This is obviously not great, but it works. An optimization should +// // be made to find the drawable in the view and just re-position. +// // If we do this, we must remember to undo the offset in OnLayout. + +// public class ButtonLayoutManager : IDisposable +// { +// // we use left/right as this does not resize the button when there is no text +// Button.ButtonContentLayout _imageOnlyLayout = new Button.ButtonContentLayout(Button.ButtonContentLayout.ImagePosition.Left, 0); + +// // reuse this instance to save on allocations +// Rect _drawableBounds = new Rect(); + +// bool _disposed; +// IButtonLayoutRenderer _renderer; +// Thickness? _defaultPaddingPix; +// Button _element; +// bool _alignIconWithText; +// bool _preserveInitialPadding; +// bool _borderAdjustsPadding; +// bool _maintainLegacyMeasurements; +// bool _hasLayoutOccurred; + +// public ButtonLayoutManager(IButtonLayoutRenderer renderer) +// : this(renderer, false, false, false, true) +// { +// } + +// public ButtonLayoutManager(IButtonLayoutRenderer renderer, +// bool alignIconWithText, +// bool preserveInitialPadding, +// bool borderAdjustsPadding, +// bool maintainLegacyMeasurements) +// { +// _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); +// _renderer.ElementChanged += OnElementChanged; +// _alignIconWithText = alignIconWithText; +// _preserveInitialPadding = preserveInitialPadding; +// _borderAdjustsPadding = borderAdjustsPadding; +// _maintainLegacyMeasurements = maintainLegacyMeasurements; +// } + +// AppCompatButton View => _renderer?.View; + +// Context Context => _renderer?.View?.Context; + +// public void Dispose() +// { +// Dispose(true); +// } + +// protected virtual void Dispose(bool disposing) +// { +// if (!_disposed) +// { +// if (disposing) +// { +// if (_renderer != null) +// { +// if (_element != null) +// { +// _element.PropertyChanged -= OnElementPropertyChanged; +// } + +// _renderer.ElementChanged -= OnElementChanged; +// _renderer = null; +// } +// } +// _disposed = true; +// } +// } + +// internal SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint) +// { +// var previousHeight = View.MeasuredHeight; +// var previousWidth = View.MeasuredWidth; + +// View.Measure(widthConstraint, heightConstraint); + +// // if the measure of the view has changed then trigger a request for layout +// // if the measure hasn't changed then force a layout of the button +// if (previousHeight != View.MeasuredHeight || previousWidth != View.MeasuredWidth) +// View.MaybeRequestLayout(); +// else +// View.ForceLayout(); + +// return new SizeRequest(new Size(View.MeasuredWidth, View.MeasuredHeight), Size.Zero); +// } + +// public void OnLayout(bool changed, int left, int top, int right, int bottom) +// { +// if (_disposed || _renderer == null || _element == null) +// return; + +// AppCompatButton view = View; +// if (view == null) +// return; + +// Drawable drawable = null; +// Drawable[] drawables = TextViewCompat.GetCompoundDrawablesRelative(view); +// if (drawables != null) +// { +// foreach (var compoundDrawable in drawables) +// { +// if (compoundDrawable != null) +// { +// drawable = compoundDrawable; +// break; +// } +// } +// } + +// if (drawable != null) +// { +// int iconWidth = drawable.IntrinsicWidth; +// drawable.CopyBounds(_drawableBounds); + +// // Center the drawable in the button if there is no text. +// // We do not need to undo this as when we get some text, the drawable recreated +// if (string.IsNullOrEmpty(_element.Text)) +// { +// var newLeft = (right - left - iconWidth) / 2 - view.PaddingLeft; + +// _drawableBounds.Set(newLeft, _drawableBounds.Top, newLeft + iconWidth, _drawableBounds.Bottom); +// drawable.Bounds = _drawableBounds; +// } +// else +// { +// if (_alignIconWithText && _element.ContentLayout.IsHorizontal()) +// { +// var buttonText = view.TextFormatted; + +// // if text is transformed, add that transformation to to ensure correct calculation of icon padding +// if (view.TransformationMethod != null) +// buttonText = view.TransformationMethod.GetTransformationFormatted(buttonText, view); + +// var measuredTextWidth = view.Paint.MeasureText(buttonText, 0, buttonText.Length()); +// var textWidth = Math.Min((int)measuredTextWidth, view.Layout.Width); +// var contentsWidth = ViewCompat.GetPaddingStart(view) + iconWidth + view.CompoundDrawablePadding + textWidth + ViewCompat.GetPaddingEnd(view); + +// var newLeft = (view.MeasuredWidth - contentsWidth) / 2; +// if (_element.ContentLayout.Position == Button.ButtonContentLayout.ImagePosition.Right) +// newLeft = -newLeft; +// if (ViewCompat.GetLayoutDirection(view) == ViewCompat.LayoutDirectionRtl) +// newLeft = -newLeft; + +// _drawableBounds.Set(newLeft, _drawableBounds.Top, newLeft + iconWidth, _drawableBounds.Bottom); +// drawable.Bounds = _drawableBounds; +// } +// } +// } + +// _hasLayoutOccurred = true; +// } + +// public void OnViewAttachedToWindow(AView attachedView) +// { +// Update(); +// } + +// public void OnViewDetachedFromWindow(AView detachedView) +// { +// } + +// public void Update() +// { +// if (!UpdateTextAndImage()) +// UpdateImage(); +// UpdatePadding(); +// } + +// void OnElementChanged(object sender, VisualElementChangedEventArgs e) +// { +// if (_element != null) +// { +// _element.PropertyChanged -= OnElementPropertyChanged; +// _element = null; +// } + +// if (e.NewElement is Button button) +// { +// _element = button; +// _element.PropertyChanged += OnElementPropertyChanged; +// } + +// Update(); +// } + +// void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) +// { +// if (_disposed || _renderer == null || _element == null) +// return; + +// if (e.PropertyName == Button.PaddingProperty.PropertyName) +// UpdatePadding(); +// else if (e.PropertyName == Button.ImageSourceProperty.PropertyName || e.PropertyName == Button.ContentLayoutProperty.PropertyName) +// UpdateImage(); +// else if (e.PropertyName == Button.TextProperty.PropertyName || e.PropertyName == VisualElement.IsVisibleProperty.PropertyName) +// UpdateTextAndImage(); +// else if (e.PropertyName == Button.BorderWidthProperty.PropertyName && _borderAdjustsPadding) +// _element.InvalidateMeasureNonVirtual(InvalidationTrigger.MeasureChanged); +// } + +// void UpdatePadding() +// { +// AppCompatButton view = View; +// if (view == null) +// return; + +// if (_disposed || _renderer == null || _element == null) +// return; + +// if (!_defaultPaddingPix.HasValue) +// _defaultPaddingPix = new Thickness(view.PaddingLeft, view.PaddingTop, view.PaddingRight, view.PaddingBottom); + +// // Currently the Padding Bindable property uses a creator factory so once it is set it can't become unset +// // I would say this is currently a bug but it's a bug that exists already in the code base. +// // Having this comment and this code more accurately demonstrates behavior then +// // having an else clause for when the PaddingProperty isn't set +// if (!_element.IsSet(Button.PaddingProperty)) +// return; + +// var padding = _element.Padding; +// var adjustment = 0.0; +// if (_borderAdjustsPadding && _element is IBorderElement borderElement && borderElement.IsBorderWidthSet() && borderElement.BorderWidth != borderElement.BorderWidthDefaultValue) +// adjustment = borderElement.BorderWidth; + +// var defaultPadding = _preserveInitialPadding && _defaultPaddingPix.HasValue +// ? _defaultPaddingPix.Value +// : new Thickness(); + +// view.SetPadding( +// (int)(Context.ToPixels(padding.Left + adjustment) + defaultPadding.Left), +// (int)(Context.ToPixels(padding.Top + adjustment) + defaultPadding.Top), +// (int)(Context.ToPixels(padding.Right + adjustment) + defaultPadding.Right), +// (int)(Context.ToPixels(padding.Bottom + adjustment) + defaultPadding.Bottom)); +// } + +// bool UpdateTextAndImage() +// { +// if (_disposed || _renderer == null || _element == null) +// return false; + +// AppCompatButton view = View; +// if (view == null) +// return false; + +// string oldText = view.Text; +// view.Text = _element.Text; + +// // If we went from or to having no text, we need to update the image position +// if (string.IsNullOrEmpty(oldText) != string.IsNullOrEmpty(view.Text)) +// { +// UpdateImage(); +// return true; +// } + +// return false; +// } + +// void UpdateImage() +// { +// if (_disposed || _renderer == null || _element == null) +// return; + +// AppCompatButton view = View; +// if (view == null) +// return; + +// ImageSource elementImage = _element.ImageSource; + +// if (elementImage == null || elementImage.IsEmpty) +// { +// view.SetCompoundDrawablesWithIntrinsicBounds(null, null, null, null); +// return; +// } + +// // No text, so no need for relative position; just center the image +// // There's no option for just plain-old centering, so we'll use Top +// // (which handles the horizontal centering) and some tricksy padding (in OnLayout) +// // to handle the vertical centering +// var layout = string.IsNullOrEmpty(_element.Text) ? _imageOnlyLayout : _element.ContentLayout; + +// if (_maintainLegacyMeasurements) +// view.CompoundDrawablePadding = (int)layout.Spacing; +// else +// view.CompoundDrawablePadding = (int)Context.ToPixels(layout.Spacing); + +// Drawable existingImage = null; +// var images = TextViewCompat.GetCompoundDrawablesRelative(view); +// for (int i = 0; i < images.Length; i++) +// if (images[i] != null) +// { +// existingImage = images[i]; +// break; +// } + +// if (_renderer is IVisualElementRenderer visualElementRenderer) +// { +// visualElementRenderer.ApplyDrawableAsync(Button.ImageSourceProperty, Context, image => +// { +// if (image == existingImage) +// return; + +// switch (layout.Position) +// { +// case Button.ButtonContentLayout.ImagePosition.Top: +// TextViewCompat.SetCompoundDrawablesRelativeWithIntrinsicBounds(view, null, image, null, null); +// break; +// case Button.ButtonContentLayout.ImagePosition.Right: +// TextViewCompat.SetCompoundDrawablesRelativeWithIntrinsicBounds(view, null, null, image, null); +// break; +// case Button.ButtonContentLayout.ImagePosition.Bottom: +// TextViewCompat.SetCompoundDrawablesRelativeWithIntrinsicBounds(view, null, null, null, image); +// break; +// default: +// // Defaults to image on the left +// TextViewCompat.SetCompoundDrawablesRelativeWithIntrinsicBounds(view, image, null, null, null); +// break; +// } + +// if (_hasLayoutOccurred) +// _element?.InvalidateMeasureNonVirtual(InvalidationTrigger.MeasureChanged); +// }); +// } +// } +// } +//} diff --git a/Maui.Core/Platform/Android/ColorExtensions.Android.cs b/Maui.Core/Platform/Android/ColorExtensions.Android.cs new file mode 100644 index 000000000000..d6931abaa52c --- /dev/null +++ b/Maui.Core/Platform/Android/ColorExtensions.Android.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; +using Android.Content; +using Android.Content.Res; +using System.Maui.Core; +#if MONOANDROID10_0 +using AndroidX.Core.Content; +#else +using Android.Support.V4.Content; +#endif + +using AColor = Android.Graphics.Color; + +namespace System.Maui.Platform +{ + public static class ColorExtensions + { + public static readonly int[][] States = { new[] { global::Android.Resource.Attribute.StateEnabled }, new[] { -global::Android.Resource.Attribute.StateEnabled } }; + + public static AColor ToNative(this Color self) + { + return new AColor((byte)(byte.MaxValue * self.R), (byte)(byte.MaxValue * self.G), (byte)(byte.MaxValue * self.B), (byte)(byte.MaxValue * self.A)); + } + + + public static AColor ToNative(this Color self, int defaultColorResourceId, Context context) + { + if (self == Color.Default) + { + return new AColor(ContextCompat.GetColor(context, defaultColorResourceId)); + } + + return ToNative(self); + } + + public static AColor ToNative(this Color self, Color defaultColor) + { + if (self == Color.Default) + return defaultColor.ToNative(); + + return ToNative(self); + } + + public static ColorStateList ToAndroidPreserveDisabled(this Color color, ColorStateList defaults) + { + int disabled = defaults.GetColorForState(States[1], color.ToNative()); + return new ColorStateList(States, new[] { color.ToNative().ToArgb(), disabled }); + } + + public static Color ToColor(this AColor color) + { + return Color.FromUint((uint)color.ToArgb()); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/Android/ContextExtensions.Android.cs b/Maui.Core/Platform/Android/ContextExtensions.Android.cs new file mode 100644 index 000000000000..39030b83f4d7 --- /dev/null +++ b/Maui.Core/Platform/Android/ContextExtensions.Android.cs @@ -0,0 +1,179 @@ +using System; +using System.Runtime.CompilerServices; +using Android.Content; +using Android.Util; +using Android.Views.InputMethods; +using AApplicationInfoFlags = Android.Content.PM.ApplicationInfoFlags; +using AActivity = Android.App.Activity; +using Size = System.Maui.Size; +#if __ANDROID_29__ +using AndroidX.Fragment.App; +using AndroidX.AppCompat.App; +using AFragmentManager = AndroidX.Fragment.App.FragmentManager; +#else +using AFragmentManager = Android.Support.V4.App.FragmentManager; +using Android.Support.V4.App; +using Android.Support.V7.App; +#endif + +namespace System.Maui.Platform +{ + public static class ContextExtensions + { + // Caching this display density here means that all pixel calculations are going to be based on the density + // of the first Context these extensions are run against. That's probably fine, but if we run into a + // situation where subsequent activities can be launched with a different display density from the intial + // activity, we'll need to remove this cached value or cache it in a Dictionary + static float s_displayDensity = float.MinValue; + + // TODO FromPixels/ToPixels is both not terribly descriptive and also possibly sort of inaccurate? + // These need better names. It's really To/From Device-Independent, but that doesn't exactly roll off the tongue. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double FromPixels(this Context self, double pixels) + { + EnsureMetrics(self); + + return pixels / s_displayDensity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size FromPixels(this Context context, double width, double height) + { + return new Size(context.FromPixels(width), context.FromPixels(height)); + } + + public static void HideKeyboard(this Context self, global::Android.Views.View view) + { + var service = (InputMethodManager)self.GetSystemService(Context.InputMethodService); + // service may be null in the context of the Android Designer + if (service != null) + service.HideSoftInputFromWindow(view.WindowToken, HideSoftInputFlags.None); + } + + public static void ShowKeyboard(this Context self, global::Android.Views.View view) + { + var service = (InputMethodManager)self.GetSystemService(Context.InputMethodService); + // Can happen in the context of the Android Designer + if (service != null) + service.ShowSoftInput(view, ShowFlags.Implicit); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ToPixels(this Context self, double dp) + { + EnsureMetrics(self); + + return (float)Math.Ceiling(dp * s_displayDensity); + } + + public static bool HasRtlSupport(this Context self) => + (self.ApplicationInfo.Flags & AApplicationInfoFlags.SupportsRtl) == AApplicationInfoFlags.SupportsRtl; + + public static int TargetSdkVersion(this Context self) => + (int)self.ApplicationInfo.TargetSdkVersion; + + internal static double GetThemeAttributeDp(this Context self, int resource) + { + using (var value = new TypedValue()) + { + if (!self.Theme.ResolveAttribute(resource, value, true)) + return -1; + + var pixels = (double)TypedValue.ComplexToDimension(value.Data, self.Resources.DisplayMetrics); + + return self.FromPixels(pixels); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void EnsureMetrics(Context context) + { + if (s_displayDensity != float.MinValue) + return; + + using (DisplayMetrics metrics = context.Resources.DisplayMetrics) + s_displayDensity = metrics.Density; + } + + public static AActivity GetActivity(this Context context) + { + if (context == null) + return null; + + if (context is AActivity activity) + return activity; + + if (context is ContextWrapper contextWrapper) + return contextWrapper.BaseContext.GetActivity(); + + return null; + } + + internal static Context GetThemedContext(this Context context) + { + if (context == null) + return null; + + if (context.IsDesignerContext()) + return context; + + if (context is AppCompatActivity activity) + return activity.SupportActionBar.ThemedContext; + + if (context is ContextWrapper contextWrapper) + return contextWrapper.BaseContext.GetThemedContext(); + + return null; + } + + static bool? _isDesignerContext; + internal static bool IsDesignerContext(this Context context) + { + if (_isDesignerContext.HasValue) + return _isDesignerContext.Value; + + context.SetDesignerContext(); + return _isDesignerContext.Value; + } + + internal static void SetDesignerContext(this Context context) + { + if (_isDesignerContext.HasValue) + return; + + if (context == null) + _isDesignerContext = false; + else if ($"{context}".Contains("com.android.layoutlib.bridge.android.BridgeContext")) + _isDesignerContext = true; + else + _isDesignerContext = false; + } + + internal static void SetDesignerContext(global::Android.Views.View view) + { + _isDesignerContext = view.IsInEditMode; + } + + internal static bool IsDesignerContext(this global::Android.Views.View view) + { + if (!_isDesignerContext.HasValue) + SetDesignerContext(view); + + return _isDesignerContext.Value; + } + + public static AFragmentManager GetFragmentManager(this Context context) + { + if (context == null) + return null; + + var activity = context.GetActivity(); + + if (activity is FragmentActivity fa) + return fa.SupportFragmentManager; + + return null; + } + } +} diff --git a/Maui.Core/Platform/Android/DrawableExtensions.Android.cs b/Maui.Core/Platform/Android/DrawableExtensions.Android.cs new file mode 100644 index 000000000000..ff8bf935b97c --- /dev/null +++ b/Maui.Core/Platform/Android/DrawableExtensions.Android.cs @@ -0,0 +1,124 @@ + +using System; +using ADrawable = Android.Graphics.Drawables.Drawable; +using AColorFilter = Android.Graphics.ColorFilter; +using AColor = Android.Graphics.Color; +#if __ANDROID_29__ +using ADrawableCompat = AndroidX.Core.Graphics.Drawable.DrawableCompat; +#else +using ADrawableCompat = Android.Support.V4.Graphics.Drawable.DrawableCompat; +#endif +using Android.Graphics; + +namespace System.Maui.Platform +{ + enum FilterMode + { + SrcIn, + Multiply, + SrcAtop + } + + internal static class DrawableExtensions + { + +#if __ANDROID_29__ + public static BlendMode GetFilterMode(FilterMode mode) + { + switch (mode) + { + case FilterMode.SrcIn: + return BlendMode.SrcIn; + case FilterMode.Multiply: + return BlendMode.Multiply; + case FilterMode.SrcAtop: + return BlendMode.SrcAtop; + } + + throw new Exception("Invalid Mode"); + } + +#else + [Obsolete] + static PorterDuff.Mode GetFilterMode(FilterMode mode) + { + return GetFilterModePre29(mode); + } +#endif + + [Obsolete] + static PorterDuff.Mode GetFilterModePre29(FilterMode mode) + { + switch (mode) + { + case FilterMode.SrcIn: + return PorterDuff.Mode.SrcIn; + case FilterMode.Multiply: + return PorterDuff.Mode.Multiply; + case FilterMode.SrcAtop: + return PorterDuff.Mode.SrcAtop; + } + + throw new Exception("Invalid Mode"); + } + + public static AColorFilter GetColorFilter(this ADrawable drawable) + { + if (drawable == null) + return null; + + return ADrawableCompat.GetColorFilter(drawable); + } + + public static void SetColorFilter(this ADrawable drawable, AColorFilter colorFilter) + { + if (drawable == null) + return; + + if (colorFilter == null) + ADrawableCompat.ClearColorFilter(drawable); + + drawable.SetColorFilter(colorFilter); + } + + + public static void SetColorFilter(this ADrawable drawable, Color color, AColorFilter defaultColorFilter, FilterMode mode) + { + if (drawable == null) + return; + + if (color == Color.Default) + { + SetColorFilter(drawable, defaultColorFilter); + return; + } + + drawable.SetColorFilter(color.ToNative(), mode); + } + + public static void SetColorFilter(this ADrawable drawable, Color color, FilterMode mode) + { + drawable.SetColorFilter(color.ToNative(), mode); + } + +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete + public static void SetColorFilter(this ADrawable drawable, AColor color, FilterMode mode) + { +#if __ANDROID_29__ + if (NativeVersion.Supports(NativeApis.BlendModeColorFilter)) + { + drawable.SetColorFilter(new BlendModeColorFilter(color, GetFilterMode(mode))); + } + else + { + drawable.SetColorFilter(color, GetFilterModePre29(mode)); + } +#else + drawable.SetColorFilter(color, GetFilterMode(mode)); +#endif + } +#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0612 // Type or member is obsolete + } +} diff --git a/Maui.Core/Platform/Android/GraphicsExtensions.Android.cs b/Maui.Core/Platform/Android/GraphicsExtensions.Android.cs new file mode 100644 index 000000000000..4a2438af1d32 --- /dev/null +++ b/Maui.Core/Platform/Android/GraphicsExtensions.Android.cs @@ -0,0 +1,89 @@ +using System.Maui.Graphics; +using Android.Graphics; +using APath = Android.Graphics.Path; +using Path = System.Maui.Graphics.Path; + +namespace System.Maui +{ + public static class GraphicsExtensions + { + public static APath AsAndroidPath(this Path path) + { + var nativePath = new APath(); + + int pointIndex = 0; + int arcAngleIndex = 0; + int arcClockwiseIndex = 0; + + foreach (var operation in path.PathOperations) + { + if (operation == PathOperation.MoveTo) + { + var point = path[pointIndex++]; + nativePath.MoveTo((float)point.X, (float)point.Y); + } + else if (operation == PathOperation.Line) + { + var point = path[pointIndex++]; + nativePath.LineTo((float)point.X, (float)point.Y); + } + + else if (operation == PathOperation.Quad) + { + var controlPoint = path[pointIndex++]; + var point = path[pointIndex++]; + nativePath.QuadTo((float)controlPoint.X, (float)controlPoint.Y, (float)point.X, (float)point.Y); + } + else if (operation == PathOperation.Cubic) + { + var controlPoint1 = path[pointIndex++]; + var controlPoint2 = path[pointIndex++]; + var point = path[pointIndex++]; + nativePath.CubicTo((float)controlPoint1.X, (float)controlPoint1.Y, (float)controlPoint2.X, (float)controlPoint2.Y, (float)point.X, + (float)point.Y); + } + else if (operation == PathOperation.Arc) + { + var topLeft = path[pointIndex++]; + var bottomRight = path[pointIndex++]; + var startAngle = path.GetArcAngle(arcAngleIndex++); + var endAngle = path.GetArcAngle(arcAngleIndex++); + var clockwise = path.IsArcClockwise(arcClockwiseIndex++); + + while (startAngle < 0) + { + startAngle += 360; + } + + while (endAngle < 0) + { + endAngle += 360; + } + + var rect = new RectF((float)topLeft.X, (float)topLeft.Y, (float)bottomRight.X, (float)bottomRight.Y); + var sweep = GraphicsOperations.GetSweep(startAngle, endAngle, clockwise); + + startAngle *= -1; + if (!clockwise) + { + sweep *= -1; + } + + nativePath.ArcTo(rect, (float)startAngle, (float)sweep); + } + else if (operation == PathOperation.Close) + { + nativePath.Close(); + } + else + { + System.Console.WriteLine("hmm"); + } + } + + return nativePath; + } + + public static Rect ToRect(this System.Maui.Rectangle r) => new Rect((int)r.Left, (int)r.Top, (int)r.Right, (int)r.Bottom); + } +} diff --git a/Maui.Core/Platform/Android/KeyboardManager.Android.cs b/Maui.Core/Platform/Android/KeyboardManager.Android.cs new file mode 100644 index 000000000000..7629ddc35331 --- /dev/null +++ b/Maui.Core/Platform/Android/KeyboardManager.Android.cs @@ -0,0 +1,27 @@ +using Android.Content; +using Android.OS; +using Android.Views.InputMethods; +using Android.Widget; +using AView = Android.Views.View; + +namespace System.Maui +{ + internal static class KeyboardManager + { + internal static void HideKeyboard(this AView inputView, bool overrideValidation = false) + { + if (inputView == null) + throw new ArgumentNullException(nameof(inputView) + " must be set before the keyboard can be hidden."); + + using (var inputMethodManager = (InputMethodManager)inputView.Context.GetSystemService(Context.InputMethodService)) + { + if (!overrideValidation && !(inputView is EditText || inputView is TextView || inputView is SearchView)) + throw new ArgumentException("inputView should be of type EditText, SearchView, or TextView"); + + IBinder windowToken = inputView.WindowToken; + if (windowToken != null && inputMethodManager != null) + inputMethodManager.HideSoftInputFromWindow(windowToken, HideSoftInputFlags.None); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/Android/LayoutViewGroup.Android.cs b/Maui.Core/Platform/Android/LayoutViewGroup.Android.cs new file mode 100644 index 000000000000..41612316cbc3 --- /dev/null +++ b/Maui.Core/Platform/Android/LayoutViewGroup.Android.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; + +namespace System.Maui.Platform +{ + public class LayoutViewGroup : ViewGroup + { + public LayoutViewGroup(Context context) : base(context) + { + } + + public LayoutViewGroup(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public LayoutViewGroup(Context context, IAttributeSet attrs) : base(context, attrs) + { + } + + public LayoutViewGroup(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + } + + public LayoutViewGroup(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes) + { + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (CrossPlatformMeasure == null) + { + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } + + var widthMode = widthMeasureSpec.GetMode(); + var heightMode = heightMeasureSpec.GetMode(); + + var width = widthMeasureSpec.GetSize(); + var height = heightMeasureSpec.GetSize(); + + var deviceIndependentWidth = Context.FromPixels(width); + var deviceIndependentHeight = Context.FromPixels(height); + + var sizeRequest = CrossPlatformMeasure(deviceIndependentWidth, deviceIndependentHeight); + + var nativeWidth = Context.ToPixels(sizeRequest.Request.Width); + var nativeHeight = Context.ToPixels(sizeRequest.Request.Height); + + SetMeasuredDimension((int)nativeWidth, (int)nativeHeight); + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (CrossPlatformArrange == null) + { + return; + } + + var deviceIndependentLeft = Context.FromPixels(l); + var deviceIndependentTop = Context.FromPixels(t); + var deviceIndependentRight = Context.FromPixels(r); + var deviceIndependentBottom = Context.FromPixels(b); + + var destination = Rectangle.FromLTRB(deviceIndependentLeft, deviceIndependentTop, + deviceIndependentRight, deviceIndependentBottom); + + CrossPlatformArrange(destination); + } + + internal Func CrossPlatformMeasure { get; set; } + internal Action CrossPlatformArrange { get; set; } + } +} diff --git a/Maui.Core/Platform/Android/MeasureSpecExtensions.Android.cs b/Maui.Core/Platform/Android/MeasureSpecExtensions.Android.cs new file mode 100644 index 000000000000..aff7444b89e7 --- /dev/null +++ b/Maui.Core/Platform/Android/MeasureSpecExtensions.Android.cs @@ -0,0 +1,26 @@ +using Android.Views; +using static Android.Views.View; + +namespace System.Maui.Platform +{ + public static class MeasureSpecExtensions + { + public static int GetSize(this int measureSpec) + { + const int modeMask = 0x3 << 30; + return measureSpec & ~modeMask; + } + + public static MeasureSpecMode GetMode(this int measureSpec) + { + return MeasureSpec.GetMode(measureSpec); + } + + // Need a method to extract mode, so we can see if the viewgroup is calling measure twice with different modes + + public static int MakeMeasureSpec(this MeasureSpecMode mode, int size) + { + return size + (int)mode; + } + } +} diff --git a/Maui.Core/Platform/Android/NativeVersion.Android.cs b/Maui.Core/Platform/Android/NativeVersion.Android.cs new file mode 100644 index 000000000000..076da2f4fac5 --- /dev/null +++ b/Maui.Core/Platform/Android/NativeVersion.Android.cs @@ -0,0 +1,32 @@ +using Android.OS; + +namespace System.Maui.Platform +{ + public static partial class NativeVersion + { + static readonly BuildVersionCodes BuildVersion = Build.VERSION.SdkInt; + + public static bool IsAtLeast(BuildVersionCodes buildVersionCode) + { + return buildVersionCode >= BuildVersion; + } + + internal static int ApiLevel { get; } = (int)BuildVersion; + + public static bool IsAtLeast(int apiLevel) + { + return ApiLevel >= apiLevel; + } + + public static bool Supports(int nativeApi) + { + return IsAtLeast(nativeApi); + } + } + + public static class NativeApis + { + public const int PowerSaveMode = 21; + public const int BlendModeColorFilter = 29; + } +} diff --git a/Maui.Core/Platform/Android/PickerManager.Android.cs b/Maui.Core/Platform/Android/PickerManager.Android.cs new file mode 100644 index 000000000000..8d690c305e26 --- /dev/null +++ b/Maui.Core/Platform/Android/PickerManager.Android.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Android.Text; +using Android.Text.Style; +using Android.Views; +using Android.Widget; +using Java.Lang; +using AView = Android.Views.View; + +namespace System.Maui.Platform +{ + internal static class PickerManager + { + readonly static HashSet AvailableKeys = new HashSet(new[] { + Keycode.Tab, Keycode.Forward, Keycode.DpadDown, Keycode.DpadLeft, Keycode.DpadRight, Keycode.DpadUp + }); + + public static void Init(EditText editText) + { + editText.Focusable = true; + editText.Clickable = true; + editText.InputType = InputTypes.Null; + editText.KeyPress += OnKeyPress; + + editText.SetOnClickListener(PickerListener.Instance); + } + + public static void OnTouchEvent(EditText sender, MotionEvent e) + { + if (e.Action == MotionEventActions.Up && !sender.IsFocused) + { + sender.RequestFocus(); + } + } + + public static void OnFocusChanged(bool gainFocus, EditText sender) + { + if (gainFocus) + sender.CallOnClick(); + } + + static void OnKeyPress(object sender, AView.KeyEventArgs e) + { + if (!AvailableKeys.Contains(e.KeyCode)) + { + e.Handled = false; + return; + } + e.Handled = true; + (sender as AView)?.CallOnClick(); + } + + public static void Dispose(EditText editText) + { + editText.KeyPress -= OnKeyPress; + editText.SetOnClickListener(null); + } + + public static ICharSequence GetTitle(Color titleColor, string title) + { + if (titleColor == Color.Default) + return new Java.Lang.String(title); + + var spannableTitle = new SpannableString(title ?? ""); + spannableTitle.SetSpan(new ForegroundColorSpan(titleColor.ToNative()), 0, spannableTitle.Length(), SpanTypes.ExclusiveExclusive); + return spannableTitle; + } + + class PickerListener : Java.Lang.Object, AView.IOnClickListener + { + public static readonly PickerListener Instance = new PickerListener(); + + public void OnClick(AView v) + { + if (v is AView picker) + picker.HideKeyboard(); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/Android/PickerView.Android.cs b/Maui.Core/Platform/Android/PickerView.Android.cs new file mode 100644 index 000000000000..3f12ba517bf8 --- /dev/null +++ b/Maui.Core/Platform/Android/PickerView.Android.cs @@ -0,0 +1,45 @@ +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; +using AndroidX.AppCompat.Widget; +using static Android.Views.View; + +namespace System.Maui.Platform +{ + public class PickerView : AppCompatTextView, IOnClickListener + { + public PickerView(Context context) : base(context) + { + Initialize(); + } + + public PickerView(Context context, IAttributeSet attrs) : base(context, attrs) + { + Initialize(); + } + + public PickerView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + Initialize(); + } + + protected PickerView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + private void Initialize() + { + Focusable = true; + SetOnClickListener(this); + } + + public Action ShowPicker { get; set; } + public Action HidePicker { get; set; } + + public void OnClick(View v) + { + ShowPicker(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/Android/RendererExtensions.Android.cs b/Maui.Core/Platform/Android/RendererExtensions.Android.cs new file mode 100644 index 000000000000..83eeeceb23fb --- /dev/null +++ b/Maui.Core/Platform/Android/RendererExtensions.Android.cs @@ -0,0 +1,27 @@ +using System; +using System.Maui.Core; +using Android.Content; +using AView = Android.Views.View; + +namespace System.Maui { + public static class RendererExtensions { + public static AView ToNative (this IFrameworkElement view, Context context) + { + if (view == null) + return null; + var handler = view.Renderer; + if (handler == null) + { + handler = Registrar.Handlers.GetHandler(view.GetType()); + if (handler is IAndroidViewRenderer arenderer) + arenderer.SetContext (context); + view.Renderer = handler; + } + handler.SetView (view); + + return handler?.ContainerView ?? handler.NativeView as AView; + + } + + } +} diff --git a/Maui.Core/Platform/Android/StepperRendererManager.Android.cs b/Maui.Core/Platform/Android/StepperRendererManager.Android.cs new file mode 100644 index 000000000000..a2363b4ade8c --- /dev/null +++ b/Maui.Core/Platform/Android/StepperRendererManager.Android.cs @@ -0,0 +1,96 @@ +using System.ComponentModel; +using Android.Views; +using AButton = Android.Widget.Button; +using AView = Android.Views.View; + +namespace System.Maui.Platform +{ + public interface IStepperRenderer + { + AButton UpButton { get; } + + AButton DownButton { get; } + + AButton CreateButton(); + + IStepper Element { get; } + } + + class StepperRendererHolder : Java.Lang.Object + { + internal IStepperRenderer _renderer; + public StepperRendererHolder(IStepperRenderer renderer) + { + _renderer = renderer; + } + } + + public static class StepperRendererManager + { + public static void CreateStepperButtons(IStepperRenderer renderer, out TButton downButton, out TButton upButton) + where TButton : AButton + { + downButton = (TButton)renderer.CreateButton(); + //downButton.Id = Platform.GenerateViewId(); + downButton.Focusable = true; + upButton = (TButton)renderer.CreateButton(); + //upButton.Id = Platform.GenerateViewId(); + upButton.Focusable = true; + + downButton.Gravity = GravityFlags.Center; + downButton.Tag = new StepperRendererHolder(renderer); + downButton.SetOnClickListener(StepperListener.Instance); + upButton.Gravity = GravityFlags.Center; + upButton.Tag = new StepperRendererHolder(renderer); + upButton.SetOnClickListener(StepperListener.Instance); + + // IMPORTANT: + // Do not be decieved. These are NOT the same characters. Neither are a "minus" either. + // The Text is a visually pleasing "minus", and the description is the phonetically correct "minus". + // The little key on your keyboard is a dash/hyphen. + downButton.Text = "-"; + downButton.ContentDescription = "−"; + + // IMPORTANT: + // Do not be decieved. These are NOT the same characters. + // The Text is a visually pleasing "plus", and the description is the phonetically correct "plus" + // (which, unlike the minus, IS found on your keyboard). + upButton.Text = "+"; + upButton.ContentDescription = "+"; + + downButton.NextFocusForwardId = upButton.Id; + } + + public static void UpdateButtons(IStepperRenderer renderer, TButton downButton, TButton upButton, PropertyChangedEventArgs e = null) + where TButton : AButton + { + if (!(renderer?.Element is IStepper stepper)) + return; + // NOTE: a value of `null` means that we are forcing an update + downButton.Enabled = stepper.IsEnabled && stepper.Value > stepper.Minimum; + upButton.Enabled = stepper.IsEnabled && stepper.Value < stepper.Maximum; + + } + + class StepperListener : Java.Lang.Object, AView.IOnClickListener + { + public static readonly StepperListener Instance = new StepperListener(); + + public void OnClick(AView v) + { + if (!(v?.Tag is StepperRendererHolder rendererHolder)) + return; + + if (!(rendererHolder._renderer?.Element is IStepper stepper)) + return; + + var increment = stepper.Increment; + if (v == rendererHolder._renderer.DownButton) + increment = -increment; + + rendererHolder._renderer.Element.Value = stepper.Value + increment; + UpdateButtons(rendererHolder._renderer, rendererHolder._renderer.DownButton, rendererHolder._renderer.UpButton); + } + } + } +} diff --git a/Maui.Core/Platform/Android/TextColorSwitcher.Android.cs b/Maui.Core/Platform/Android/TextColorSwitcher.Android.cs new file mode 100644 index 000000000000..d2ab238ead9b --- /dev/null +++ b/Maui.Core/Platform/Android/TextColorSwitcher.Android.cs @@ -0,0 +1,64 @@ +using Android.Content.Res; +using Android.Widget; + +namespace System.Maui.Platform +{ + /// + /// Handles color state management for a TextView's TextColor and HintTextColor properties + /// + internal class TextColorSwitcher + { + static readonly int[][] s_colorStates = { new[] { global::Android.Resource.Attribute.StateEnabled }, new[] { -global::Android.Resource.Attribute.StateEnabled } }; + + readonly ColorStateList _defaultTextColors; + Color _currentTextColor; + + readonly ColorStateList _defaultHintTextColors; + Color _currentHintTextColor = Color.Default; + + public TextView Control { get; } + + public TextColorSwitcher(TextView control) + { + Control = control; + _defaultTextColors = control.TextColors; + _defaultHintTextColors = control.HintTextColors; + } + + public void UpdateTextColor(Color color) + { + if (color == _currentTextColor) + return; + + _currentTextColor = color; + + if (color.IsDefault) + { + Control.SetTextColor(_defaultTextColors); + } + else + { + var acolor = color.ToNative().ToArgb(); + Control.SetTextColor(new ColorStateList(s_colorStates, new[] { acolor, acolor })); + } + } + + public void UpdateHintTextColor(Color color) + { + if (color == _currentHintTextColor) + return; + + _currentTextColor = color; + + if (color.IsDefault) + { + Control.SetHintTextColor(_defaultHintTextColors); + } + else + { + var acolor = color.ToNative().ToArgb(); + Control.SetHintTextColor(new ColorStateList(s_colorStates, new[] { acolor, acolor })); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/Android/ViewExtensions.Android.cs b/Maui.Core/Platform/Android/ViewExtensions.Android.cs new file mode 100644 index 000000000000..3e78cbde2647 --- /dev/null +++ b/Maui.Core/Platform/Android/ViewExtensions.Android.cs @@ -0,0 +1,22 @@ +using Android.Content.Res; + +namespace System.Maui.Platform +{ + public static class ViewExtensions + { + public static void SetTextColor(this AndroidX.AppCompat.Widget.AppCompatButton button, Color color, Color defaultColor) + => button.SetTextColor(color.Cleanse(defaultColor).ToNative()); + + public static void SetTextColor(this AndroidX.AppCompat.Widget.AppCompatButton button, Color color, ColorStateList defaultColor) + { + if (color.IsDefault) + button.SetTextColor(defaultColor); + else + button.SetTextColor(color.ToNative()); + } + static Color Cleanse(this Color color, Color defaultColor) => color.IsDefault ? defaultColor : color; + + public static void SetText(this AndroidX.AppCompat.Widget.AppCompatButton button, string text) + => button.Text = text; + } +} diff --git a/Maui.Core/Platform/Android/ViewGroupExtensions.Android.cs b/Maui.Core/Platform/Android/ViewGroupExtensions.Android.cs new file mode 100644 index 000000000000..9ce8978b1624 --- /dev/null +++ b/Maui.Core/Platform/Android/ViewGroupExtensions.Android.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using AView = Android.Views.View; +using AViewGroup = Android.Views.ViewGroup; + +namespace System.Maui.Platform +{ + internal static class ViewGroupExtensions + { + internal static IEnumerable GetChildrenOfType(this AViewGroup self) where T : AView + { + for (var i = 0; i < self.ChildCount; i++) + { + AView child = self.GetChildAt(i); + var typedChild = child as T; + if (typedChild != null) + yield return typedChild; + + if (child is AViewGroup) + { + IEnumerable myChildren = (child as AViewGroup).GetChildrenOfType(); + foreach (T nextChild in myChildren) + yield return nextChild; + } + } + } + } +} diff --git a/Maui.Core/Platform/Mac/RendererExtensions.Mac.cs b/Maui.Core/Platform/Mac/RendererExtensions.Mac.cs new file mode 100644 index 000000000000..a57d8f0194ab --- /dev/null +++ b/Maui.Core/Platform/Mac/RendererExtensions.Mac.cs @@ -0,0 +1,26 @@ +using System; +using AppKit; + +namespace System.Maui +{ + public static class RendererExtensions + { + public static NSView ToNative(this IView view) + { + if (view == null) + return null; + var handler = view.Renderer; + if (handler == null) + { + handler = Registrar.Handlers.GetHandler(view.GetType()) as IViewRenderer; + view.Renderer = handler; + } + if (handler == null) + throw new InvalidOperationException("No handler was registered for this view type"); + + handler.SetView(view); + + return handler.ContainerView ?? handler.NativeView as NSView; + } + } +} diff --git a/Maui.Core/Platform/Mac/ViewExtensions.Mac.cs b/Maui.Core/Platform/Mac/ViewExtensions.Mac.cs new file mode 100644 index 000000000000..4588f66cee4b --- /dev/null +++ b/Maui.Core/Platform/Mac/ViewExtensions.Mac.cs @@ -0,0 +1,38 @@ +using System; +using Foundation; +using AppKit; + +namespace System.Maui.Platform +{ + public static class ViewExtensions + { + public static void SetText(this NSTextField label, string text) + => label.StringValue = text; + + public static void SetText(this NSTextField label, NSAttributedString text) + => label.AttributedStringValue = text; + + public static void SetText(this NSButton view, string text) + => view.StringValue = text; + + public static void SetText(this NSButton view, NSAttributedString text) + => view.AttributedStringValue = text; + + public static void SetBackgroundColor(this NSView view, NSColor color) + { + view.WantsLayer = true; + view.Layer.BackgroundColor = color.CGColor; + } + + public static NSColor GetBackgroundColor (this NSView view) => + NSColor.FromCGColor(view.Layer.BackgroundColor); + + public static CoreGraphics.CGSize SizeThatFits(this NSView view, CoreGraphics.CGSize size) => + (view as NSControl)?.SizeThatFits(size) ?? view.FittingSize; + + public static void SetTextColor(this NSButton button, Color color, Color defaultColor) => + button.ContentTintColor = color.Cleanse(defaultColor).ToNativeColor(); + + static Color Cleanse(this Color color, Color defaultColor) => color.IsDefault ? defaultColor : color; + } +} diff --git a/Maui.Core/Platform/MaciOS/ColorExtensions.MaciOS.cs b/Maui.Core/Platform/MaciOS/ColorExtensions.MaciOS.cs new file mode 100644 index 000000000000..1195934d5fac --- /dev/null +++ b/Maui.Core/Platform/MaciOS/ColorExtensions.MaciOS.cs @@ -0,0 +1,136 @@ +using CoreGraphics; +using PointF = CoreGraphics.CGPoint; +using RectangleF = CoreGraphics.CGRect; +using SizeF = CoreGraphics.CGSize; +using System.Maui.Core; +#if __MOBILE__ +using UIKit; +#else +using AppKit; +using UIColor = AppKit.NSColor; +#endif +namespace System.Maui.Platform +{ + public static class ColorExtensions + { +#if __MOBILE__ + internal static readonly UIColor Black = UIColor.Black; + internal static readonly UIColor SeventyPercentGrey = new UIColor(0.7f, 0.7f, 0.7f, 1); +#else + internal static readonly NSColor Black = NSColor.Black; + internal static readonly NSColor SeventyPercentGrey = NSColor.FromRgba(0.7f, 0.7f, 0.7f, 1); +#endif + + public static CGColor ToCGColor(this Color color) + { +#if __MOBILE__ + return color.ToNativeColor ().CGColor; +#else + return color.ToNativeColor().CGColor; +#endif + } + +#if __MOBILE__ + public static UIColor FromPatternImageFromBundle(string bgImage) + { + var image = UIImage.FromBundle(bgImage); + if (image == null) + return UIColor.White; + + return UIColor.FromPatternImage(image); + } +#endif + + public static Color ToColor(this UIColor color) + { + nfloat red; + nfloat green; + nfloat blue; + nfloat alpha; +#if __MOBILE__ + color.GetRGBA(out red, out green, out blue, out alpha); +#else + color.GetRgba(out red, out green, out blue, out alpha); +#endif + return new Color(red, green, blue, alpha); + } + +#if __MOBILE__ + public static UIColor ToNativeColor(this Color color) + { + return new UIColor((float)color.R, (float)color.G, (float)color.B, (float)color.A); + } + + public static UIColor ToNativeColor (this Color color, Color defaultColor) + { + if (color.IsDefault) + return defaultColor.ToNativeColor (); + + return color.ToNativeColor (); + } + + public static UIColor ToNativeColor (this Color color, UIColor defaultColor) + { + if (color.IsDefault) + return defaultColor; + + return color.ToNativeColor (); + } +#else + public static NSColor ToNativeColor(this Color color) + { + return NSColor.FromRgba((float)color.R, (float)color.G, (float)color.B, (float)color.A); + } + + public static NSColor ToNativeColor(this Color color, Color defaultColor) + { + if (color.IsDefault) + return defaultColor.ToNativeColor(); + + return color.ToNativeColor(); + } + + public static NSColor ToNativeColor(this Color color, NSColor defaultColor) + { + if (color.IsDefault) + return defaultColor; + + return color.ToNativeColor(); + } +#endif + } + + //public static class PointExtensions + //{ + // public static Point ToPoint(this PointF point) + // { + // return new Point(point.X, point.Y); + // } + + // public static PointF ToPointF(this Point point) + // { + // return new PointF(point.X, point.Y); + // } + //} + + //public static class SizeExtensions + //{ + // public static SizeF ToSizeF(this Size size) + // { + // return new SizeF((float)size.Width, (float)size.Height); + // } + //} + + //public static class RectangleExtensions + //{ + // public static Rectangle ToRectangle(this RectangleF rect) + // { + // return new Rectangle(rect.X, rect.Y, rect.Width, rect.Height); + // } + + // public static RectangleF ToRectangleF(this Rectangle rect) + // { + // return new RectangleF((nfloat)rect.X, (nfloat)rect.Y, (nfloat)rect.Width, (nfloat)rect.Height); + // } + //} +} \ No newline at end of file diff --git a/Maui.Core/Platform/MaciOS/CoreGraphicsExtensions.MaciOS.cs b/Maui.Core/Platform/MaciOS/CoreGraphicsExtensions.MaciOS.cs new file mode 100644 index 000000000000..fa85566f9925 --- /dev/null +++ b/Maui.Core/Platform/MaciOS/CoreGraphicsExtensions.MaciOS.cs @@ -0,0 +1,119 @@ +using CoreGraphics; + +namespace System.Maui.Platform +{ + public static class CoreGraphicsExtensions + { + public static Point ToPoint(this CGPoint size) + { + return new Point((float)size.X, (float)size.Y); + } + + public static Size ToSize(this CGSize size) + { + return new Size((float)size.Width, (float)size.Height); + } + + public static CGSize ToCGSize(this Size size) + { + return new CGSize(size.Width, size.Height); + } + + public static Rectangle ToRectangle(this CGRect rect) + { + return new Rectangle((float)rect.X, (float)rect.Y, (float)rect.Width, (float)rect.Height); + } + + public static CGRect ToCGRect(this Rectangle rect) + { + return new CGRect(rect.X, rect.Y, rect.Width, rect.Height); + } + + //public static CGPath ToCGPath( + // this PathF target) + //{ + // var path = new CGPath(); + + // int pointIndex = 0; + // int arcAngleIndex = 0; + // int arcClockwiseIndex = 0; + + // foreach (var operation in target.PathOperations) + // { + // if (operation == PathOperation.MoveTo) + // { + // var point = target[pointIndex++]; + // path.MoveToPoint(point.X, point.Y); + // } + // else if (operation == PathOperation.Line) + // { + // var endPoint = target[pointIndex++]; + // path.AddLineToPoint(endPoint.X, endPoint.Y); + + // } + + // else if (operation == PathOperation.Quad) + // { + // var controlPoint = target[pointIndex++]; + // var endPoint = target[pointIndex++]; + // path.AddQuadCurveToPoint( + // controlPoint.X, + // controlPoint.Y, + // endPoint.X, + // endPoint.Y); + // } + // else if (operation == PathOperation.Cubic) + // { + // var controlPoint1 = target[pointIndex++]; + // var controlPoint2 = target[pointIndex++]; + // var endPoint = target[pointIndex++]; + // path.AddCurveToPoint( + // controlPoint1.X, + // controlPoint1.Y, + // controlPoint2.X, + // controlPoint2.Y, + // endPoint.X, + // endPoint.Y); + // } + // else if (operation == PathOperation.Arc) + // { + // var topLeft = target[pointIndex++]; + // var bottomRight = target[pointIndex++]; + // float startAngle = target.GetArcAngle(arcAngleIndex++); + // float endAngle = target.GetArcAngle(arcAngleIndex++); + // var clockwise = target.IsArcClockwise(arcClockwiseIndex++); + + // var startAngleInRadians = GraphicsOperations.DegreesToRadians(-startAngle); + // var endAngleInRadians = GraphicsOperations.DegreesToRadians(-endAngle); + + // while (startAngleInRadians < 0) + // { + // startAngleInRadians += (float)Math.PI * 2; + // } + + // while (endAngleInRadians < 0) + // { + // endAngleInRadians += (float)Math.PI * 2; + // } + + // var cx = (bottomRight.X + topLeft.X) / 2; + // var cy = (bottomRight.Y + topLeft.Y) / 2; + // var width = bottomRight.X - topLeft.X; + // var height = bottomRight.Y - topLeft.Y; + // var r = width / 2; + + // var transform = CGAffineTransform.MakeTranslation(cx, cy); + // transform = CGAffineTransform.Multiply(CGAffineTransform.MakeScale(1, height / width), transform); + + // path.AddArc(transform, 0, 0, r, startAngleInRadians, endAngleInRadians, !clockwise); + // } + // else if (operation == PathOperation.Close) + // { + // path.CloseSubpath(); + // } + // } + + // return path; + //} + } +} diff --git a/Maui.Core/Platform/MaciOS/DateTimeExtensions.MaciOS.cs b/Maui.Core/Platform/MaciOS/DateTimeExtensions.MaciOS.cs new file mode 100644 index 000000000000..ddfab61be738 --- /dev/null +++ b/Maui.Core/Platform/MaciOS/DateTimeExtensions.MaciOS.cs @@ -0,0 +1,18 @@ +using System; +using Foundation; + +namespace System.Maui.Platform +{ + public static class DateExtensions + { + public static DateTime ToDateTime(this NSDate date) + { + return new DateTime(2001, 1, 1, 0, 0, 0).AddSeconds(date.SecondsSinceReferenceDate); + } + + public static NSDate ToNSDate(this DateTime date) + { + return NSDate.FromTimeIntervalSinceReferenceDate((date - new DateTime(2001, 1, 1, 0, 0, 0)).TotalSeconds); + } + } +} diff --git a/Maui.Core/Platform/MaciOS/NSAttributedTextExtensions.MaciOS.cs b/Maui.Core/Platform/MaciOS/NSAttributedTextExtensions.MaciOS.cs new file mode 100644 index 000000000000..a7ca6dba2d33 --- /dev/null +++ b/Maui.Core/Platform/MaciOS/NSAttributedTextExtensions.MaciOS.cs @@ -0,0 +1,35 @@ +using Foundation; + +#if __MOBILE__ +using UIKit; +#else +using AppKit; +#endif + +namespace System.Maui.Platform +{ + public static class NSAttributedTextExtensions + { + static NSAttributedStringDocumentAttributes htmlAttributes = new NSAttributedStringDocumentAttributes + { + DocumentType = NSDocumentType.HTML, + StringEncoding = NSStringEncoding.UTF8 + }; + public static NSAttributedString ToNSAttributedString(this string html) + { + +#if __MOBILE__ + + NSError nsError = null; + + return new NSAttributedString(html, htmlAttributes, ref nsError); +#else + var htmlData = new NSMutableData(); + htmlData.SetData(html); + + return new NSAttributedString(html, htmlAttributes, out _); +#endif + } + + } +} diff --git a/Maui.Core/Platform/Standard/ViewExtensions.Standard.cs b/Maui.Core/Platform/Standard/ViewExtensions.Standard.cs new file mode 100644 index 000000000000..a4c04f48e0d8 --- /dev/null +++ b/Maui.Core/Platform/Standard/ViewExtensions.Standard.cs @@ -0,0 +1,9 @@ +using System; +namespace System.Maui.Platform +{ + internal static class ViewExtensions + { + public static void SetText(this object obj, string text) { } + public static void SetTextColor(this object obj, Color color, Color defaultColor) { } + } +} diff --git a/Maui.Core/Platform/Win32/ColorExtensions.Win32.cs b/Maui.Core/Platform/Win32/ColorExtensions.Win32.cs new file mode 100644 index 000000000000..79209747e63d --- /dev/null +++ b/Maui.Core/Platform/Win32/ColorExtensions.Win32.cs @@ -0,0 +1,17 @@ +using System.Windows.Media; + +namespace System.Maui.Platform +{ + public static class ColorExtensions + { + public static Brush ToBrush(this Color color) + { + return new SolidColorBrush(color.ToMediaColor()); + } + + public static System.Windows.Media.Color ToMediaColor(this Color color) + { + return System.Windows.Media.Color.FromArgb((byte)(color.A * 255), (byte)(color.R * 255), (byte)(color.G * 255), (byte)(color.B * 255)); + } + } +} diff --git a/Maui.Core/Platform/Win32/FrameworkElementExtensions.Win32.cs b/Maui.Core/Platform/Win32/FrameworkElementExtensions.Win32.cs new file mode 100644 index 000000000000..3a7e922a3d54 --- /dev/null +++ b/Maui.Core/Platform/Win32/FrameworkElementExtensions.Win32.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; + +namespace System.Maui.Platform +{ + internal static class FrameworkElementExtensions + { + public static object UpdateDependencyColor(this DependencyObject depo, DependencyProperty dp, Color newColor) + { + if (!newColor.IsDefault) + depo.SetValue(dp, newColor.ToBrush()); + else + depo.ClearValue(dp); + + return depo.GetValue(dp); + } + + internal static IEnumerable GetChildren(this DependencyObject parent) where T : DependencyObject + { + int myChildrenCount = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < myChildrenCount; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T) + yield return child as T; + else + { + foreach (var subChild in child.GetChildren()) + yield return subChild; + } + } + } + } +} diff --git a/Maui.Core/Platform/Win32/LayoutPanel.Win32.cs b/Maui.Core/Platform/Win32/LayoutPanel.Win32.cs new file mode 100644 index 000000000000..7b2736111c9e --- /dev/null +++ b/Maui.Core/Platform/Win32/LayoutPanel.Win32.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Controls; + +namespace System.Maui.Platform +{ + public class LayoutPanel : Canvas + { + internal Func CrossPlatformMeasure { get; set; } + internal Action CrossPlatformArrange { get; set; } + + protected override Windows.Size MeasureOverride(Windows.Size constraint) + { + if (CrossPlatformMeasure == null) + { + return base.MeasureOverride(constraint); + } + + var sizeRequest = CrossPlatformMeasure(constraint.Width, constraint.Height); + + return new Windows.Size(sizeRequest.Request.Width, sizeRequest.Request.Height); + } + + protected override Windows.Size ArrangeOverride(Windows.Size arrangeSize) + { + if (CrossPlatformArrange == null) + { + return base.ArrangeOverride(arrangeSize); + } + + CrossPlatformArrange(new Rectangle(0, 0, arrangeSize.Width, arrangeSize.Height)); + + return arrangeSize; + } + } +} diff --git a/Maui.Core/Platform/Win32/LockableObservableListWrapper,Win32.cs b/Maui.Core/Platform/Win32/LockableObservableListWrapper,Win32.cs new file mode 100644 index 000000000000..f2de51ad96e5 --- /dev/null +++ b/Maui.Core/Platform/Win32/LockableObservableListWrapper,Win32.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace System.Core.Platform +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class LockableObservableListWrapper : IList, ICollection, INotifyCollectionChanged, INotifyPropertyChanged, IReadOnlyList, IReadOnlyCollection, IEnumerable, IEnumerable + { + public readonly ObservableCollection _list = new ObservableCollection(); + + event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged + { + add { ((INotifyCollectionChanged)_list).CollectionChanged += value; } + remove { ((INotifyCollectionChanged)_list).CollectionChanged -= value; } + } + + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add { ((INotifyPropertyChanged)_list).PropertyChanged += value; } + remove { ((INotifyPropertyChanged)_list).PropertyChanged -= value; } + } + + public bool IsLocked { get; set; } + + void ThrowOnLocked() + { + if (IsLocked) + throw new InvalidOperationException("The Items list can not be manipulated if the ItemsSource property is set"); + } + + public string this[int index] + { + get { return _list[index]; } + set + { + ThrowOnLocked(); + _list[index] = value; + } + } + + public int Count + { + get { return _list.Count; } + } + + public bool IsReadOnly + { + get { return ((IList)_list).IsReadOnly; } + } + + public void InternalAdd(string item) + { + _list.Add(item); + } + + public void Add(string item) + { + ThrowOnLocked(); + InternalAdd(item); + } + + public void InternalClear() + { + _list.Clear(); + } + + public void Clear() + { + ThrowOnLocked(); + InternalClear(); + } + + public bool Contains(string item) + { + return _list.Contains(item); + } + + public void CopyTo(string[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + public int IndexOf(string item) + { + return _list.IndexOf(item); + } + + public void InternalInsert(int index, string item) + { + _list.Insert(index, item); + } + + public void Insert(int index, string item) + { + ThrowOnLocked(); + InternalInsert(index, item); + } + + public bool InternalRemove(string item) + { + return _list.Remove(item); + } + + public bool Remove(string item) + { + ThrowOnLocked(); + return InternalRemove(item); + } + + public void InternalRemoveAt(int index) + { + _list.RemoveAt(index); + } + + public void RemoveAt(int index) + { + ThrowOnLocked(); + InternalRemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + } +} diff --git a/Maui.Core/Platform/Win32/RendererExtensions.Win32.cs b/Maui.Core/Platform/Win32/RendererExtensions.Win32.cs new file mode 100644 index 000000000000..4e56416b87d0 --- /dev/null +++ b/Maui.Core/Platform/Win32/RendererExtensions.Win32.cs @@ -0,0 +1,26 @@ +using System.Windows; + +namespace System.Maui.Platform +{ + public static class RendererExtensions + { + public static FrameworkElement ToNative(this IView view) + { + if (view == null) + return null; + + var handler = view.Renderer; + if (handler == null) + { + handler = Registrar.Handlers.GetHandler(view.GetType()); + view.Renderer = handler; + } + + handler.SetView(view); + + return handler?.ContainerView ?? handler.NativeView as FrameworkElement; + + } + + } +} diff --git a/Maui.Core/Platform/Win32/ViewExtensions.Win32.cs b/Maui.Core/Platform/Win32/ViewExtensions.Win32.cs new file mode 100644 index 000000000000..e4e52d602baf --- /dev/null +++ b/Maui.Core/Platform/Win32/ViewExtensions.Win32.cs @@ -0,0 +1,17 @@ +using System.Maui.Core.Controls; +using WButton = System.Windows.Controls.Button; + +namespace System.Maui.Platform +{ + internal static class ViewExtensions + { + public static void SetText(this MauiButton button, string text) + { + button.Content = text; + } + public static void SetTextColor(this MauiButton button, Color color, Color defaultColor) + { + button.UpdateDependencyColor(WButton.ForegroundProperty, color); + } + } +} diff --git a/Maui.Core/Platform/iOS/FormattedStringExtensions.iOS.cs b/Maui.Core/Platform/iOS/FormattedStringExtensions.iOS.cs new file mode 100644 index 000000000000..e390c73a6e0a --- /dev/null +++ b/Maui.Core/Platform/iOS/FormattedStringExtensions.iOS.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + class FormattedStringExtensions + { + } +} diff --git a/Maui.Core/Platform/iOS/GraphicsExtensions.iOS.cs b/Maui.Core/Platform/iOS/GraphicsExtensions.iOS.cs new file mode 100644 index 000000000000..3ffd19efe19b --- /dev/null +++ b/Maui.Core/Platform/iOS/GraphicsExtensions.iOS.cs @@ -0,0 +1,121 @@ +using System; +using System.Maui.Graphics; +using CoreGraphics; + +namespace System.Maui +{ + public static class CoreGraphicsExtensions + { + //public static Point ToPointF(this CGPoint size) + //{ + // return new Point(size.X, size.Y); + //} + + //public static Size ToSizeF(this CGSize size) + //{ + // return new Size(size.Width, size.Height); + //} + + //public static CGSize ToCGSize(this Size size) + //{ + // return new CGSize(size.Width, size.Height); + //} + + //public static Rectangle ToRectangleF(this CGRect rect) + //{ + // return new Rectangle(rect.X, rect.Y, rect.Width, rect.Height); + //} + + //public static CGRect ToCGRect(this Rectangle rect) + //{ + // return new CGRect(rect.X, rect.Y, rect.Width, rect.Height); + //} + + public static CGPath ToCGPath( + this Path target) + { + var path = new CGPath(); + + int pointIndex = 0; + int arcAngleIndex = 0; + int arcClockwiseIndex = 0; + + foreach (var operation in target.PathOperations) + { + if (operation == PathOperation.MoveTo) + { + var point = target[pointIndex++]; + path.MoveToPoint((nfloat)point.X, (nfloat)point.Y); + } + else if (operation == PathOperation.Line) + { + var endPoint = target[pointIndex++]; + path.AddLineToPoint((nfloat)endPoint.X, (nfloat)endPoint.Y); + + } + + else if (operation == PathOperation.Quad) + { + var controlPoint = target[pointIndex++]; + var endPoint = target[pointIndex++]; + path.AddQuadCurveToPoint( + (nfloat)controlPoint.X, + (nfloat)controlPoint.Y, + (nfloat)endPoint.X, + (nfloat)endPoint.Y); + } + else if (operation == PathOperation.Cubic) + { + var controlPoint1 = target[pointIndex++]; + var controlPoint2 = target[pointIndex++]; + var endPoint = target[pointIndex++]; + path.AddCurveToPoint( + (nfloat)controlPoint1.X, + (nfloat)controlPoint1.Y, + (nfloat)controlPoint2.X, + (nfloat)controlPoint2.Y, + (nfloat)endPoint.X, + (nfloat)endPoint.Y); + } + else if (operation == PathOperation.Arc) + { + var topLeft = target[pointIndex++]; + var bottomRight = target[pointIndex++]; + var startAngle = target.GetArcAngle(arcAngleIndex++); + var endAngle = target.GetArcAngle(arcAngleIndex++); + var clockwise = target.IsArcClockwise(arcClockwiseIndex++); + + var startAngleInRadians = GraphicsOperations.DegreesToRadians(-startAngle); + var endAngleInRadians = GraphicsOperations.DegreesToRadians(-endAngle); + + while (startAngleInRadians < 0) + { + startAngleInRadians += Math.PI * 2; + } + + while (endAngleInRadians < 0) + { + endAngleInRadians += Math.PI * 2; + } + + var cx = (bottomRight.X + topLeft.X) / 2; + var cy = (bottomRight.Y + topLeft.Y) / 2; + var width = bottomRight.X - topLeft.X; + var height = bottomRight.Y - topLeft.Y; + var r = width / 2; + + var transform = CGAffineTransform.MakeTranslation((nfloat)cx, (nfloat)cy); + transform = CGAffineTransform.Multiply(CGAffineTransform.MakeScale(1, (nfloat)height / (nfloat)width), transform); + + path.AddArc(transform, 0, 0, (nfloat)r, (nfloat)startAngleInRadians, (nfloat)endAngleInRadians, !clockwise); + } + else if (operation == PathOperation.Close) + { + path.CloseSubpath(); + } + } + + return path; + } + } +} diff --git a/Maui.Core/Platform/iOS/LayoutView.iOS.cs b/Maui.Core/Platform/iOS/LayoutView.iOS.cs new file mode 100644 index 000000000000..7bac85500347 --- /dev/null +++ b/Maui.Core/Platform/iOS/LayoutView.iOS.cs @@ -0,0 +1,37 @@ +using CoreGraphics; +using UIKit; + +namespace System.Maui.Platform +{ + public class LayoutView : UIView + { + public override CGSize SizeThatFits(CGSize size) + { + if (CrossPlatformMeasure == null) + { + return base.SizeThatFits(size); + } + + var width = size.Width; + var height = size.Height; + + var sizeRequest = CrossPlatformMeasure(width, height); + + return base.SizeThatFits(sizeRequest.Request.ToCGSize()); + } + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + var width = Frame.Width; + var height = Frame.Height; + CrossPlatformMeasure(width, height); + + CrossPlatformArrange?.Invoke(Frame.ToRectangle()); + } + + internal Func CrossPlatformMeasure { get; set; } + internal Action CrossPlatformArrange { get; set; } + } +} diff --git a/Maui.Core/Platform/iOS/NativeVersion.iOS.cs b/Maui.Core/Platform/iOS/NativeVersion.iOS.cs new file mode 100644 index 000000000000..55c4872abd80 --- /dev/null +++ b/Maui.Core/Platform/iOS/NativeVersion.iOS.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UIKit; + +namespace System.Maui.Platform +{ + public static class NativeVersion + { + public static bool IsAtLeast(int version) + { + return UIDevice.CurrentDevice.CheckSystemVersion(version, 0); + } + + private static bool? SetNeedsUpdateOfHomeIndicatorAutoHidden; + + public static bool Supports(string capability) + { + switch (capability) + { + case NativeApis.RespondsToSetNeedsUpdateOfHomeIndicatorAutoHidden: + if (!SetNeedsUpdateOfHomeIndicatorAutoHidden.HasValue) + { + SetNeedsUpdateOfHomeIndicatorAutoHidden = new UIViewController().RespondsToSelector(new ObjCRuntime.Selector("setNeedsUpdateOfHomeIndicatorAutoHidden")); + } + return SetNeedsUpdateOfHomeIndicatorAutoHidden.Value; + } + + return false; + } + + public static bool Supports(int capability) + { + return IsAtLeast(capability); + } + } + + public static class NativeApis + { + public const string RespondsToSetNeedsUpdateOfHomeIndicatorAutoHidden = "RespondsToSetNeedsUpdateOfHomeIndicatorAutoHidden"; + public const int UIActivityIndicatorViewStyleMedium = 13; + } +} diff --git a/Maui.Core/Platform/iOS/PickerView.iOS.cs b/Maui.Core/Platform/iOS/PickerView.iOS.cs new file mode 100644 index 000000000000..6ebff779fa22 --- /dev/null +++ b/Maui.Core/Platform/iOS/PickerView.iOS.cs @@ -0,0 +1,32 @@ +using UIKit; + +namespace System.Maui.Platform +{ + public class PickerView : UILabel + { + private UIView _inputView; + private UIView _inputAccessoryView; + + public PickerView() + { + UserInteractionEnabled = true; + UITapGestureRecognizer tapGesture = new UITapGestureRecognizer(() => BecomeFirstResponder()); + AddGestureRecognizer(tapGesture); + } + + public void SetInputView(UIView inputView) + { + _inputView = inputView; + } + + public void SetInputAccessoryView(UIView inputAccessoryView) + { + _inputAccessoryView = inputAccessoryView; + } + + public override UIView InputView => _inputView ?? base.InputView; + public override UIView InputAccessoryView => _inputAccessoryView ?? base.InputAccessoryView; + + public override bool CanBecomeFirstResponder => true; + } +} diff --git a/Maui.Core/Platform/iOS/RendererExtensions.iOS.cs b/Maui.Core/Platform/iOS/RendererExtensions.iOS.cs new file mode 100644 index 000000000000..99b094820a05 --- /dev/null +++ b/Maui.Core/Platform/iOS/RendererExtensions.iOS.cs @@ -0,0 +1,22 @@ +using UIKit; + +namespace System.Maui { + public static class RendererExtensions { + public static UIView ToNative(this IView view) + { + if (view == null) + return null; + var handler = view.Renderer; + if (handler == null) { + handler = Registrar.Handlers.GetHandler (view.GetType ()) as IViewRenderer; + view.Renderer = handler; + } + if(handler == null) + throw new InvalidOperationException("No handler was registered for this view type"); + + handler.SetView (view); + + return handler.ContainerView ?? handler.NativeView as UIView; + } + } +} diff --git a/Maui.Core/Platform/iOS/UIApplicationExtensions.iOS.cs b/Maui.Core/Platform/iOS/UIApplicationExtensions.iOS.cs new file mode 100644 index 000000000000..3341a5926063 --- /dev/null +++ b/Maui.Core/Platform/iOS/UIApplicationExtensions.iOS.cs @@ -0,0 +1,26 @@ +using UIKit; + +namespace System.Maui.Core.Platform +{ + internal static class UIApplicationExtensions + { + public static UIWindow GetKeyWindow(this UIApplication application) + { +#if __MOBILE__ + var windows = application.Windows; + + for (int i = 0; i < windows.Length; i++) + { + var window = windows[i]; + if (window.IsKeyWindow) + return window; + } + + return null; +#else + return application.KeyWindow; +#endif + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Platform/iOS/UIViewExtensions.iOS.cs b/Maui.Core/Platform/iOS/UIViewExtensions.iOS.cs new file mode 100644 index 000000000000..1bd93ce6c769 --- /dev/null +++ b/Maui.Core/Platform/iOS/UIViewExtensions.iOS.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using UIKit; + +namespace System.Maui.Platform +{ + internal static class UIViewExtensions + { + internal static T FindDescendantView(this UIView view) where T : UIView + { + var queue = new Queue(); + queue.Enqueue(view); + + while (queue.Count > 0) + { + var descendantView = queue.Dequeue(); + + var result = descendantView as T; + if (result != null) + return result; + + for (var i = 0; i < descendantView.Subviews?.Length; i++) + queue.Enqueue(descendantView.Subviews[i]); + } + + return null; + } + } +} diff --git a/Maui.Core/Platform/iOS/ViewExtensions.iOS.cs b/Maui.Core/Platform/iOS/ViewExtensions.iOS.cs new file mode 100644 index 000000000000..324af0527aef --- /dev/null +++ b/Maui.Core/Platform/iOS/ViewExtensions.iOS.cs @@ -0,0 +1,33 @@ +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public static class ViewExtensions + { + public static void SetText(this UILabel label, string text) + => label.Text = text; + + public static void SetText(this UILabel label, NSAttributedString text) + => label.AttributedText = text; + + public static void SetBackgroundColor (this UIView view, UIColor color) + => view.BackgroundColor = color; + public static UIColor GetBackgroundColor (this UIView view) => + view.BackgroundColor; + + + public static void SetText(this UIButton view, string text) + => view.SetTitle(text, UIControlState.Normal); + + public static void SetText(this UIButton view, NSAttributedString text) + => view.SetAttributedTitle(text, UIControlState.Normal); + + public static void SetTextColor(this UIButton button, Color color, Color defaultColor) + { + button.SetTitleColor(color.Cleanse(defaultColor).ToNativeColor(), UIControlState.Normal); + } + + static Color Cleanse(this Color color, Color defaultColor) => color.IsDefault ? defaultColor : color; + } +} diff --git a/Maui.Core/PropertyMapper.cs b/Maui.Core/PropertyMapper.cs new file mode 100644 index 000000000000..38ec3d883298 --- /dev/null +++ b/Maui.Core/PropertyMapper.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace System.Maui +{ + public class PropertyMapper { + internal Dictionary Action, bool RunOnUpdateAll)> genericMap = new Dictionary action, bool runOnUpdateAll)> (); + protected virtual void UpdateProperty (string key, IViewRenderer viewRenderer, IFrameworkElement virtualView) + { + if (genericMap.TryGetValue(key, out var action)) + { + action.Action?.Invoke(viewRenderer, virtualView); + } + } + public void UpdateProperty (IViewRenderer viewRenderer, IFrameworkElement virtualView, string property) + { + if (virtualView == null) + return; + UpdateProperty (property, viewRenderer, virtualView); + } + public void UpdateProperties (IViewRenderer viewRenderer, IFrameworkElement virtualView) + { + if (virtualView == null) + return; + foreach (var key in Keys) { + UpdateProperty (key, viewRenderer, virtualView); + } + } + public virtual ICollection Keys => genericMap.Keys; + } + + public class PropertyMapper : PropertyMapper, IEnumerable + where TVirtualView : IFrameworkElement { + private PropertyMapper chained; + public PropertyMapper Chained { + get => chained; + set { + chained = value; + cachedKeys = null; + } + } + + ICollection cachedKeys; + public override ICollection Keys => cachedKeys ??= (Chained?.Keys.Union (keysForStartup) as ICollection ?? genericMap.Keys); + ICollection keysForStartup => genericMap.Where(x => x.Value.RunOnUpdateAll).Select(x => x.Key).ToList(); + + public int Count => Keys.Count; + + public bool IsReadOnly => false; + + public Action this [string key] { + set => genericMap [key] = ((r, v) => value?.Invoke (r, (TVirtualView)v),true); + } + + public PropertyMapper () + { + } + + public PropertyMapper (PropertyMapper chained) + { + Chained = chained; + } + ActionMapper actions; + public ActionMapper Actions { + get => actions ??= new ActionMapper(this); + } + + + + protected override void UpdateProperty (string key, IViewRenderer viewRenderer, IFrameworkElement virtualView) + { + if (genericMap.TryGetValue (key, out var action)) + action.Action?.Invoke (viewRenderer, virtualView); + else + Chained?.UpdateProperty (viewRenderer, virtualView, key); + } + + public void Add (string key, Action action) + => this [key] = action; + + public void Add(string key, Action action, bool ignoreOnStartup) + =>genericMap[key] = ((r, v) => action?.Invoke(r, (TVirtualView)v), ignoreOnStartup); + + + + + IEnumerator IEnumerable.GetEnumerator () => genericMap.GetEnumerator (); + + public class ActionMapper + where TView : TVirtualView, IFrameworkElement + { + public ActionMapper(PropertyMapper propertyMapper) + { + PropertyMapper = propertyMapper; + } + + public PropertyMapper PropertyMapper { get; } + + public Action this[string key] + { + set => PropertyMapper.genericMap[key] = ((r, v) => value?.Invoke(r, (TView)v), false); + } + } + + } + +} diff --git a/Maui.Core/Registrar.cs b/Maui.Core/Registrar.cs new file mode 100644 index 000000000000..b1643ac69658 --- /dev/null +++ b/Maui.Core/Registrar.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics; +using System.Maui.Core; + +namespace System.Maui +{ + public static class Registrar + { + public static Registrar Handlers { get; private set; } + public static Registrar Registered { get; private set; } + public static Dictionary Effects { get; } = new Dictionary(); + //public static Dictionary> StyleProperties { get; } = new Dictionary>(); + + static Registrar() + { + Handlers = new Registrar (); + } + } + public class Registrar + { + internal Dictionary Handler = new Dictionary(); + + public void Register() + where TView : TType + where TRender : TTypeRender + { + Register(typeof(TView), typeof(TRender)); + } + + public void Register(Type view, Type handler) + { + Handler[view] = handler; + } + public TTypeRender GetHandler() + { + return GetHandler(typeof(T)); + } + + internal List> GetViewType(Type type) => + Handler.Where(x => isType(x.Value,type)).ToList(); + bool isType(Type type, Type type2) + { + if (type == type2) + return true; + if (!type.IsGenericType) + return false; + var paramerter = type.GetGenericArguments(); + return paramerter[0] == type2; + } + + public TTypeRender GetHandler(Type type) + { + List types = new List { type }; + Type baseType = type.BaseType; + while (baseType != null) + { + types.Add(baseType); + baseType = baseType.BaseType; + } + + foreach (var t in types) + { + var renderer = getRenderer(t); + if (renderer != null) + return renderer; + } + return default(TTypeRender); + } + + public Type GetRendererType(Type type) + { + List types = new List { type }; + Type baseType = type.BaseType; + while (baseType != null) + { + types.Add(baseType); + baseType = baseType.BaseType; + } + + foreach (var t in types) + { + if (Handler.TryGetValue(t, out var returnType)) + return returnType; + } + return null; + } + + TTypeRender getRenderer(Type t) + { + if (!Handler.TryGetValue(t, out var renderer)) + return default(TTypeRender); + try + { + var newObject = Activator.CreateInstance(renderer); + return (TTypeRender)newObject; + } + catch (Exception ex) + { + if (Debugger.IsAttached) + throw ex; + } + + return default; + } + } +} diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Android.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Android.cs new file mode 100644 index 000000000000..3e6b95bb2bf5 --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Android.cs @@ -0,0 +1,34 @@ +using Android.Views; +using AProgressBar = Android.Widget.ProgressBar; + +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer : AbstractViewRenderer + { + protected override AProgressBar CreateView() + { + return new AProgressBar(Context) { Indeterminate = true }; + } + + public static void MapPropertyIsRunning(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer.NativeView is AProgressBar aProgressBar)) + return; + + aProgressBar.Visibility = activityIndicator.IsRunning ? ViewStates.Visible : ViewStates.Invisible; + } + + public static void MapPropertyColor(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer.NativeView is AProgressBar aProgressBar)) + return; + + Color color = activityIndicator.Color; + + if (!color.IsDefault) + aProgressBar.IndeterminateDrawable?.SetColorFilter(color.ToNative(), FilterMode.SrcIn); + else + aProgressBar.IndeterminateDrawable?.ClearColorFilter(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Mac.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Mac.cs new file mode 100644 index 000000000000..f649225cae53 --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Mac.cs @@ -0,0 +1,65 @@ +using System.Linq; +using AppKit; +using CoreGraphics; +using CoreImage; + +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer : AbstractViewRenderer + { + CIColorPolynomial _currentColorFilter; + NSColor _currentColor; + + protected override NSProgressIndicator CreateView() + { + return new NSProgressIndicator(CGRect.Empty) { Style = NSProgressIndicatorStyle.Spinning }; + } + + public static void MapPropertyIsRunning(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer.NativeView is NSProgressIndicator nSProgressIndicator)) + return; + + if (activityIndicator.IsRunning) + nSProgressIndicator.StartAnimation(renderer.ContainerView); + else + nSProgressIndicator.StopAnimation(renderer.ContainerView); + } + + public static void MapPropertyColor(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer is ActivityIndicatorRenderer activityIndicatorRenderer) || !(renderer.NativeView is NSProgressIndicator nSProgressIndicator)) + return; + + var color = activityIndicator.Color; + + if (activityIndicatorRenderer._currentColorFilter == null && color.IsDefault) + return; + + if (color.IsDefault) + nSProgressIndicator.ContentFilters = new CIFilter[0]; + + var newColor = activityIndicator.Color.ToNativeColor(); + + if (Equals(activityIndicatorRenderer._currentColor, newColor)) + { + if (nSProgressIndicator.ContentFilters?.FirstOrDefault() != activityIndicatorRenderer._currentColorFilter) + { + nSProgressIndicator.ContentFilters = new CIFilter[] { activityIndicatorRenderer._currentColorFilter }; + } + return; + } + + activityIndicatorRenderer._currentColor = newColor; + + activityIndicatorRenderer._currentColorFilter = new CIColorPolynomial + { + RedCoefficients = new CIVector(activityIndicatorRenderer._currentColor.RedComponent), + BlueCoefficients = new CIVector(activityIndicatorRenderer._currentColor.BlueComponent), + GreenCoefficients = new CIVector(activityIndicatorRenderer._currentColor.GreenComponent) + }; + + nSProgressIndicator.ContentFilters = new CIFilter[] { activityIndicatorRenderer._currentColorFilter }; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Standard.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Standard.cs new file mode 100644 index 000000000000..b482cacb195a --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Standard.cs @@ -0,0 +1,10 @@ +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyIsRunning(IViewRenderer renderer, IActivityIndicator activityIndicator) { } + public static void MapPropertyColor(IViewRenderer renderer, IActivityIndicator activityIndicator) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Win32.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Win32.cs new file mode 100644 index 000000000000..89cb59289ded --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.Win32.cs @@ -0,0 +1,21 @@ +using System.Maui.Core.Controls; + +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer : AbstractViewRenderer + { + protected override MauiProgressRing CreateView() => new MauiProgressRing(); + + public static void MapPropertyIsRunning(IViewRenderer renderer, IActivityIndicator activityIndicator) => (renderer as ActivityIndicatorRenderer)?.UpdateIsActive(); + public static void MapPropertyColor(IViewRenderer renderer, IActivityIndicator activityIndicator) => (renderer as ActivityIndicatorRenderer)?.UpdateColor(); + public virtual void UpdateColor() + { + TypedNativeView.UpdateDependencyColor(MauiProgressRing.ForegroundProperty, !VirtualView.Color.IsDefault ? VirtualView.Color : Color.Accent); + } + + public virtual void UpdateIsActive() + { + TypedNativeView.IsActive = VirtualView.IsRunning; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.cs new file mode 100644 index 000000000000..b242c2bf56a6 --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.cs @@ -0,0 +1,21 @@ +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer + { + public static PropertyMapper ActivityIndicatorMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IActivityIndicator.Color)] = MapPropertyColor, + [nameof(IActivityIndicator.IsRunning)] = MapPropertyIsRunning + }; + + public ActivityIndicatorRenderer() : base(ActivityIndicatorMapper) + { + + } + + public ActivityIndicatorRenderer(PropertyMapper mapper) : base(mapper ?? ActivityIndicatorMapper) + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.iOS.cs b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.iOS.cs new file mode 100644 index 000000000000..233f7a9f613b --- /dev/null +++ b/Maui.Core/Renderers/ActivityIndicator/ActivityIndicatorRenderer.iOS.cs @@ -0,0 +1,38 @@ +using CoreGraphics; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class ActivityIndicatorRenderer : AbstractViewRenderer + { + protected override UIActivityIndicatorView CreateView() + { +#if __XCODE11__ + if(NativeVersion.Supports(NativeApi.UIActivityIndicatorViewStyleMedium)) + return new UIActivityIndicatorView(CGRect.Empty) { ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Medium }; + else +#endif + return new UIActivityIndicatorView(CGRect.Empty) { ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray }; + } + + public static void MapPropertyIsRunning(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer.NativeView is UIActivityIndicatorView uIActivityIndicatorView)) + return; + + if (activityIndicator.IsRunning) + uIActivityIndicatorView.StartAnimating(); + else + uIActivityIndicatorView.StopAnimating(); + } + + public static void MapPropertyColor(IViewRenderer renderer, IActivityIndicator activityIndicator) + { + if (!(renderer.NativeView is UIActivityIndicatorView uIActivityIndicatorView)) + return; + + if (!uIActivityIndicatorView.IsAnimating && activityIndicator.IsRunning) + uIActivityIndicatorView.StartAnimating(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.Android.cs b/Maui.Core/Renderers/Button/ButtonRenderer.Android.cs new file mode 100644 index 000000000000..3186e9feae38 --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.Android.cs @@ -0,0 +1,71 @@ +using Android.Content; +using Android.Graphics; +using Android.Util; +using Android.Views; + +#if __ANDROID_29__ +using AndroidX.Core.View; +using AndroidX.AppCompat.Widget; +#else +using Android.Support.V4.View; +using Android.Support.V7.Widget; +#endif + +using AColor = Android.Graphics.Color; +using AView = Android.Views.View; +//using AndroidX.AppCompat.Widget; +using AndroidX.Core.Content.Resources; +using static Android.Content.Res.Resources; +using Android; +using AndroidX.Core.Content; +using Android.Content.Res; + +namespace System.Maui.Platform +{ + public partial class ButtonRenderer + { + protected override AppCompatButton CreateView() + { + var button = new AppCompatButton(Context); + button.Click += Button_Click; + return button; + } + + //static ColorStateList ColorStateList; + + protected override void SetupDefaults() + { + var colors = TypedNativeView.TextColors; + DefaultTextColor = new AColor(colors.DefaultColor).ToColor(); + } + + private void Button_Click(object sender, EventArgs e) + { + VirtualView.Clicked(); + } + + protected override void DisposeView(AppCompatButton nativeView) + { + nativeView.Click -= Button_Click; + base.DisposeView(nativeView); + } + + + //public static void MapPropertyButtonFont(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonInputTransparent(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonCharacterSpacing(IViewRenderer renderer, IButton view) + //{ + + //} + + + } +} diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.Mac.cs b/Maui.Core/Renderers/Button/ButtonRenderer.Mac.cs new file mode 100644 index 000000000000..ac565b6097d3 --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.Mac.cs @@ -0,0 +1,33 @@ +using AppKit; + +namespace System.Maui.Platform +{ + public partial class ButtonRenderer + { + protected override NSButton CreateView() + { + var button = new NSButton(); + return button; + } + + protected override void SetupDefaults() + { + if(TypedNativeView.ContentTintColor != null) + DefaultTextColor = TypedNativeView.ContentTintColor.ToColor(); + } + + //public static void MapPropertyButtonFont(IViewRenderer renderer, IButton view) + //{ + + //} + //public static void MapPropertyButtonInputTransparent(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonCharacterSpacing(IViewRenderer renderer, IButton view) + //{ + + //} + } +} diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.Standard.cs b/Maui.Core/Renderers/Button/ButtonRenderer.Standard.cs new file mode 100644 index 000000000000..8daa5afa2f1b --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.Standard.cs @@ -0,0 +1,23 @@ +using System; +namespace System.Maui.Platform { + public partial class ButtonRenderer { + + //public static void MapPropertyButtonFont(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonInputTransparent(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonCharacterSpacing(IViewRenderer renderer, IButton view) + //{ + + //} + + protected override object CreateView() => throw new NotImplementedException(); + + } +} diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.Win32.cs b/Maui.Core/Renderers/Button/ButtonRenderer.Win32.cs new file mode 100644 index 000000000000..b7e25fae8fb7 --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.Win32.cs @@ -0,0 +1,26 @@ +using System; +using System.Maui.Core.Controls; +using System.Windows; + +namespace System.Maui.Platform { + public partial class ButtonRenderer + { + protected override MauiButton CreateView() + { + var control = new MauiButton(); + control.Click += HandleButtonClick; + return control; + } + + protected override void DisposeView(MauiButton nativeView) + { + nativeView.Click -= HandleButtonClick; + base.DisposeView(nativeView); + } + + void HandleButtonClick(object sender, RoutedEventArgs e) + { + VirtualView.Clicked(); + } + } +} diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.cs b/Maui.Core/Renderers/Button/ButtonRenderer.cs new file mode 100644 index 000000000000..643eabcedc8e --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.cs @@ -0,0 +1,47 @@ +#if __IOS__ +using NativeView = UIKit.UIButton; +#elif __MACOS__ +using NativeView = AppKit.NSButton; +#elif MONOANDROID +using NativeView = AndroidX.AppCompat.Widget.AppCompatButton; +#elif NETCOREAPP +using NativeView = System.Maui.Core.Controls.MauiButton; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform { + public partial class ButtonRenderer : AbstractViewRenderer + { + public static Color DefaultTextColor { get; private set; } + + public static PropertyMapper ButtonMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IButton.Text)] = MapPropertyText, + [nameof(IButton.Color)] = MapPropertyTextColor, + //[nameof(IButton.Font)] = MapPropertyButtonFont, + //[nameof(IButton.InputTransparent)] = MapPropertyButtonInputTransparent, + //[nameof(IButton.CharacterSpacing)] = MapPropertyButtonCharacterSpacing + }; + + public ButtonRenderer () : base (ButtonMapper) + { + + } + public ButtonRenderer (PropertyMapper mapper) : base (mapper ?? ButtonMapper) + { + + } + + public static void MapPropertyText(IViewRenderer renderer, IButton view) + { + var button = renderer.NativeView as NativeView; + button.SetText(view.Text); + } + public static void MapPropertyTextColor(IViewRenderer renderer, IButton view) + { + var button = renderer.NativeView as NativeView; + button.SetTextColor(view.Color, DefaultTextColor); + } + } +} diff --git a/Maui.Core/Renderers/Button/ButtonRenderer.iOS.cs b/Maui.Core/Renderers/Button/ButtonRenderer.iOS.cs new file mode 100644 index 000000000000..2a92d112f5c2 --- /dev/null +++ b/Maui.Core/Renderers/Button/ButtonRenderer.iOS.cs @@ -0,0 +1,52 @@ +using UIKit; + +namespace System.Maui.Platform { + public partial class ButtonRenderer { + protected override UIButton CreateView() + { + var button = UIButton.FromType(UIButtonType.RoundedRect); + button.TouchUpInside += Button_TouchUpInside; + button.BackgroundColor = UIColor.Green; + return button; + } + + protected override void SetupDefaults() + { + DefaultTextColor = TypedNativeView.CurrentTitleColor.ToColor(); + } + + private void Button_TouchUpInside (object sender, EventArgs e) + { + this.VirtualView.Clicked (); + } + + public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + { + var size = TypedNativeView.SizeThatFits (new CoreGraphics.CGSize (widthConstraint, heightConstraint)); + return new SizeRequest( new Size(size.Width, size.Height)); + } + + protected override void DisposeView (UIButton nativeView) + { + nativeView.TouchUpInside -= Button_TouchUpInside; + base.DisposeView (nativeView); + } + + + //public static void MapPropertyButtonFont(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonInputTransparent(IViewRenderer renderer, IButton view) + //{ + + //} + + //public static void MapPropertyButtonCharacterSpacing(IViewRenderer renderer, IButton view) + //{ + + //} + + } +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Android.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Android.cs new file mode 100644 index 000000000000..1faf1623d2e1 --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Android.cs @@ -0,0 +1,24 @@ +#if __IOS__ +using NativeView = UIKit.UIButton; +#elif __MACOS__ +using NativeView = AppKit.NSButton; +#elif MONOANDROID +using NativeView = AndroidX.AppCompat.Widget.AppCompatCheckBox; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform { + public partial class CheckBoxRenderer + { + protected override NativeView CreateView() + { + var checkBox = new NativeView(Context); + checkBox.CheckedChange += OnCheckChanged; + return checkBox; + } + + void OnCheckChanged(object sender, Android.Widget.CompoundButton.CheckedChangeEventArgs e) => + VirtualView.IsChecked = e.IsChecked; + } +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Mac.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Mac.cs new file mode 100644 index 000000000000..2bafb14d9024 --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Mac.cs @@ -0,0 +1,21 @@ +using System; +#if __IOS__ +using NativeView = UIKit.UIButton; +#elif __MACOS__ +using NativeView = AppKit.NSButton; +#elif MONOANDROID +using NativeView = AndroidX.AppCompat.Widget.AppCompatCheckBox; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform { + public partial class CheckBoxRenderer + { + protected override NativeView CreateView() + { + var checkBox = new NativeView(); + return checkBox; + } + } +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Standard.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Standard.cs new file mode 100644 index 000000000000..2bafb14d9024 --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Standard.cs @@ -0,0 +1,21 @@ +using System; +#if __IOS__ +using NativeView = UIKit.UIButton; +#elif __MACOS__ +using NativeView = AppKit.NSButton; +#elif MONOANDROID +using NativeView = AndroidX.AppCompat.Widget.AppCompatCheckBox; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform { + public partial class CheckBoxRenderer + { + protected override NativeView CreateView() + { + var checkBox = new NativeView(); + return checkBox; + } + } +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Win32.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Win32.cs new file mode 100644 index 000000000000..1f88aa3ec652 --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.Win32.cs @@ -0,0 +1,57 @@ +using System.Maui.Core.Controls; +using System.Windows.Controls; +using System.Windows.Media; + +namespace System.Maui.Platform +{ + public partial class CheckBoxRenderer + { + static Brush _tintDefaultBrush = Color.Transparent.ToBrush(); + + protected override CheckBox CreateView() + { + var checkBox = new MauiCheckBox() + { + //Style = (System.Windows.Style)System.Windows.Application.Current.MainWindow.FindResource("FormsCheckBoxStyle") + }; + checkBox.Checked += OnChecked; + checkBox.Unchecked += OnChecked; + return checkBox; + } + + protected override void DisposeView(CheckBox nativeView) + { + nativeView.Checked -= OnChecked; + nativeView.Unchecked -= OnChecked; + + base.DisposeView(nativeView); + } + + public virtual void UpdateColor() + { + var color = VirtualView.Color; + + var control = TypedNativeView as MauiCheckBox; + + if (control == null) + return; + + if (color.IsDefault) + control.TintBrush = _tintDefaultBrush; + else + control.TintBrush = color.ToBrush(); + + } + + void UpdateIsChecked() + { + TypedNativeView.IsChecked = VirtualView.IsChecked; + } + + void OnChecked(object sender, System.Windows.RoutedEventArgs e) + { + VirtualView.IsChecked = TypedNativeView.IsChecked.HasValue ? TypedNativeView.IsChecked.Value : false; + } + } + +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.cs new file mode 100644 index 000000000000..91ecb3626a05 --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.cs @@ -0,0 +1,47 @@ +#if __IOS__ +using NativeView = System.Maui.Platform.MauiCheckBox; +#elif __MACOS__ +using NativeView = AppKit.NSButton; +#elif MONOANDROID +using NativeView = AndroidX.AppCompat.Widget.AppCompatCheckBox; +#elif NETCOREAPP +using NativeView = System.Windows.Controls.CheckBox; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform { + public partial class CheckBoxRenderer : AbstractViewRenderer + { + public static Color DefaultTextColor { get; private set; } + + public static PropertyMapper ButtonMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(ICheckBox.IsChecked)] = MapPropertyIsChecked, + [nameof(ICheckBox.Color)] = MapPropertyColor + }; + + public CheckBoxRenderer() : base (ButtonMapper) + { + + } + public CheckBoxRenderer(PropertyMapper mapper) : base (mapper ?? ButtonMapper) + { + + } + + public static void MapPropertyIsChecked(IViewRenderer renderer, ICheckBox view) + { +#if NETCOREAPP + (renderer as CheckBoxRenderer)?.UpdateIsChecked(); +#endif + } + + public static void MapPropertyColor(IViewRenderer renderer, ICheckBox view) + { +#if NETCOREAPP + (renderer as CheckBoxRenderer)?.UpdateColor(); +#endif + } + } +} diff --git a/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.iOS.cs b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.iOS.cs new file mode 100644 index 000000000000..d48486b62e6e --- /dev/null +++ b/Maui.Core/Renderers/CheckBox/CheckBoxRenderer.iOS.cs @@ -0,0 +1,261 @@ +using CoreGraphics; +using UIKit; +using NativeView = System.Maui.Platform.MauiCheckBox; + +namespace System.Maui.Platform { + public partial class CheckBoxRenderer + { + protected override NativeView CreateView() + { + var checkBox = new MauiCheckBox(); + checkBox.CheckedChanged += OnCheckChanged; + return checkBox; + } + + void OnCheckChanged(object sender, EventArgs e) => + VirtualView.IsChecked = this.TypedNativeView.IsChecked; + } + + + public class MauiCheckBox : UIButton + { + static UIImage _checked; + static UIImage _unchecked; + + // all these values were chosen to just match the android drawables that are used + const float _defaultSize = 18.0f; + const float _lineWidth = 2.0f; + Color _tintColor; + bool _isChecked; + bool _isEnabled; + float _minimumViewSize; + public EventHandler CheckedChanged; + bool _disposed; + + internal float MinimumViewSize + { + get { return _minimumViewSize; } + set + { + _minimumViewSize = value; + var xOffset = (value - _defaultSize + _lineWidth) / 4; + ContentEdgeInsets = new UIEdgeInsets(0, xOffset, 0, 0); + } + } + + public MauiCheckBox() + { + TouchUpInside += OnTouchUpInside; + ContentMode = UIViewContentMode.Center; + ImageView.ContentMode = UIViewContentMode.ScaleAspectFit; + HorizontalAlignment = UIControlContentHorizontalAlignment.Left; + VerticalAlignment = UIControlContentVerticalAlignment.Center; + AdjustsImageWhenDisabled = false; + AdjustsImageWhenHighlighted = false; + } + + void OnTouchUpInside(object sender, EventArgs e) + { + IsChecked = !IsChecked; + CheckedChanged?.Invoke(this, null); + } + + public bool IsChecked + { + get => _isChecked; + set + { + if (value == _isChecked) + return; + + _isChecked = value; + UpdateDisplay(); + } + } + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (value == _isEnabled) + return; + + _isEnabled = value; + UserInteractionEnabled = IsEnabled; + UpdateDisplay(); + } + } + + public Color CheckBoxTintColor + { + get => _tintColor; + set + { + if (_tintColor == value) + return; + + _tintColor = value; + CheckBoxTintUIColor = (CheckBoxTintColor.IsDefault ? null : CheckBoxTintColor.ToNativeColor()); + } + } + + UIColor _checkBoxTintUIColor; + UIColor CheckBoxTintUIColor + { + get + { + return _checkBoxTintUIColor ?? UIColor.White; + } + set + { + if (value == _checkBoxTintUIColor) + return; + + _checkBoxTintUIColor = value; + ImageView.TintColor = value; + TintColor = value; + + if (Enabled) + SetNeedsDisplay(); + else + UpdateDisplay(); + } + } + + public override bool Enabled + { + get + { + return base.Enabled; + } + + set + { + bool changed = base.Enabled != value; + base.Enabled = value; + + if (changed) + UpdateDisplay(); + } + } + + protected virtual UIImage GetCheckBoximage() + { + // Ideally I would use the static images here but when disabled it always tints them grey + // and I don't know how to make it not tint them gray + if (!Enabled && CheckBoxTintColor != Color.Default) + { + if (IsChecked) + return CreateCheckBox(CreateCheckMark()).ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); + + return CreateCheckBox(null).ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); + } + + if (_checked == null) + _checked = CreateCheckBox(CreateCheckMark()).ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + + if (_unchecked == null) + _unchecked = CreateCheckBox(null).ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + + return IsChecked ? _checked : _unchecked; + } + + internal void UpdateDisplay() + { + SetImage(GetCheckBoximage(), UIControlState.Normal); + SetNeedsDisplay(); + } + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + UpdateDisplay(); + } + + internal virtual UIBezierPath CreateBoxPath(CGRect backgroundRect) => UIBezierPath.FromOval(backgroundRect); + internal virtual UIBezierPath CreateCheckPath() => new UIBezierPath + { + LineWidth = (nfloat)0.077, + LineCapStyle = CGLineCap.Round, + LineJoinStyle = CGLineJoin.Round + }; + + internal virtual void DrawCheckMark(UIBezierPath path) + { + path.MoveTo(new CGPoint(0.72f, 0.22f)); + path.AddLineTo(new CGPoint(0.33f, 0.6f)); + path.AddLineTo(new CGPoint(0.15f, 0.42f)); + } + + internal virtual UIImage CreateCheckBox(UIImage check) + { + UIGraphics.BeginImageContextWithOptions(new CGSize(_defaultSize, _defaultSize), false, 0); + var context = UIGraphics.GetCurrentContext(); + context.SaveState(); + + var checkedColor = CheckBoxTintUIColor; + checkedColor.SetFill(); + checkedColor.SetStroke(); + + var vPadding = _lineWidth / 2; + var hPadding = _lineWidth / 2; + var diameter = _defaultSize - _lineWidth; + + var backgroundRect = new CGRect(hPadding, vPadding, diameter, diameter); + var boxPath = CreateBoxPath(backgroundRect); + boxPath.LineWidth = _lineWidth; + boxPath.Stroke(); + + if (check != null) + { + boxPath.Fill(); + check.Draw(new CGPoint(0, 0), CGBlendMode.DestinationOut, 1); + } + + context.RestoreState(); + var img = UIGraphics.GetImageFromCurrentImageContext(); + UIGraphics.EndImageContext(); + + return img; + } + + + internal UIImage CreateCheckMark() + { + UIGraphics.BeginImageContextWithOptions(new CGSize(_defaultSize, _defaultSize), false, 0); + var context = UIGraphics.GetCurrentContext(); + context.SaveState(); + + var vPadding = _lineWidth / 2; + var hPadding = _lineWidth / 2; + var diameter = _defaultSize - _lineWidth; + + var checkPath = CreateCheckPath(); + + context.TranslateCTM(hPadding + (nfloat)(0.05 * diameter), vPadding + (nfloat)(0.1 * diameter)); + context.ScaleCTM(diameter, diameter); + DrawCheckMark(checkPath); + UIColor.White.SetStroke(); + checkPath.Stroke(); + + context.RestoreState(); + var img = UIGraphics.GetImageFromCurrentImageContext(); + UIGraphics.EndImageContext(); + + return img; + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + if (disposing) + TouchUpInside -= OnTouchUpInside; + + base.Dispose(disposing); + } + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Android.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Android.cs new file mode 100644 index 000000000000..0ca729970e82 --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Android.cs @@ -0,0 +1,107 @@ +using Android.App; +using AndroidX.AppCompat.Widget; + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer : AbstractViewRenderer + { + TextColorSwitcher _textColorSwitcher; + DatePickerDialog _dialog; + + protected override AppCompatTextView CreateView() + { + var text = new PickerView(Context) + { + HidePicker = HidePickerDialog, + ShowPicker = ShowPickerDialog + }; + + _textColorSwitcher = new TextColorSwitcher(text); + return text; + } + + protected override void DisposeView(AppCompatTextView nativeView) + { + _textColorSwitcher = null; + if (_dialog != null) + { + _dialog.Hide(); + _dialog = null; + } + + base.DisposeView(nativeView); + } + + public static void MapPropertyMaximumDate(IViewRenderer renderer, IDatePicker datePicker) + { + (renderer as DatePickerRenderer)?.UpdateMaximumDate(); + } + + public static void MapPropertyMinimumDate(IViewRenderer renderer, IDatePicker datePicker) + { + (renderer as DatePickerRenderer)?.UpdateMinimumDate(); + } + + protected virtual DatePickerDialog CreateDatePickerDialog(int year, int month, int day) + { + void onDateSetCallback(object obj, DatePickerDialog.DateSetEventArgs args) + { + VirtualView.SelectedDate = args.Date; + TypedNativeView.Text = VirtualView.Text; + } + + var dialog = new DatePickerDialog(Context, onDateSetCallback, year, month, day); + + return dialog; + } + + private void ShowPickerDialog() + { + var date = VirtualView.SelectedDate; + ShowPickerDialog(date.Year, date.Month, date.Day); + } + + // This overload is here so we can pass in the current values from the dialog + // on an orientation change (so that orientation changes don't cause the user's date selection progress + // to be lost). Not useful until we have orientation changed events. + private void ShowPickerDialog(int year, int month, int day) + { + _dialog = CreateDatePickerDialog(year, month, day); + + UpdateMinimumDate(); + UpdateMaximumDate(); + + _dialog.Show(); + } + + private void HidePickerDialog() + { + _dialog?.Hide(); + } + + private long ConvertDate(DateTime date) + { + return (long)date.ToUniversalTime().Subtract(DateTime.MinValue.AddYears(1969)).TotalMilliseconds; + } + + private void UpdateMaximumDate() + { + if (_dialog == null) + { + return; + } + + _dialog.DatePicker.MaxDate = ConvertDate(VirtualView.MaximumDate); + } + + private void UpdateMinimumDate() + { + if (_dialog == null) + { + return; + } + + _dialog.DatePicker.MinDate = ConvertDate(VirtualView.MinimumDate); + } + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Mac.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Mac.cs new file mode 100644 index 000000000000..579ab24049aa --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Mac.cs @@ -0,0 +1,79 @@ +using AppKit; +using Foundation; + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer : AbstractViewRenderer + { + //NSColor _defaultTextColor; + //NSColor _defaultBackgroundColor; + + protected override NSDatePicker CreateView() + { + var nativeView = new MauiNSDatePicker + { + DatePickerMode = NSDatePickerMode.Single, + TimeZone = new NSTimeZone("UTC"), + DatePickerStyle = NSDatePickerStyle.TextFieldAndStepper, + DatePickerElements = NSDatePickerElementFlags.YearMonthDateDay + }; + + nativeView.ValidateProposedDateValue += HandleValueChanged; + + return nativeView; + } + + protected override void DisposeView(NSDatePicker nativeView) + { + nativeView.ValidateProposedDateValue -= HandleValueChanged; + base.DisposeView(nativeView); + } + + public static void MapPropertyMaximumDate(IViewRenderer renderer, IDatePicker datePicker) { } + public static void MapPropertyMinimumDate(IViewRenderer renderer, IDatePicker datePicker) { } + public static void MapPropertySelectedDate(IViewRenderer renderer, IDatePicker datePicker) { + (renderer as DatePickerRenderer)?.UpdateSelectedDate(); + } + + public virtual void UpdateSelectedDate() + { + var dt = VirtualView.SelectedDate.Date; + if (TypedNativeView.DateValue.ToDateTime().Date != dt) + TypedNativeView.DateValue = dt.ToNSDate(); + } + + void HandleValueChanged(object sender, NSDatePickerValidatorEventArgs e) + { + VirtualView.SelectedDate = e.ProposedDateValue.ToDateTime().Date; + } + } + + internal class MauiNSDatePicker : NSDatePicker + { + public event EventHandler FocusChanged; + + public override bool ResignFirstResponder() + { + FocusChanged?.Invoke(this, new BoolEventArgs(false)); + return base.ResignFirstResponder(); + } + public override bool BecomeFirstResponder() + { + FocusChanged?.Invoke(this, new BoolEventArgs(true)); + return base.BecomeFirstResponder(); + } + } + + internal class BoolEventArgs : EventArgs + { + public BoolEventArgs(bool value) + { + Value = value; + } + public bool Value + { + get; + private set; + } + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Standard.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Standard.cs new file mode 100644 index 000000000000..ed07a678b4fa --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Standard.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyMaximumDate(IViewRenderer renderer, IDatePicker datePicker) { } + public static void MapPropertyMinimumDate(IViewRenderer renderer, IDatePicker datePicker) { } + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Win32.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Win32.cs new file mode 100644 index 000000000000..c32986235748 --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.Win32.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WDatePicker = System.Windows.Controls.DatePicker; + + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer : AbstractViewRenderer + { + protected override WDatePicker CreateView() + { + var control = new WDatePicker(); + control.SelectedDateChanged += OnSelectedDateChanged; + return control; + } + + public static void MapPropertyMaximumDate(IViewRenderer renderer, IDatePicker datePicker) => (renderer as DatePickerRenderer)?.UpdateMaximumDate(); + public static void MapPropertyMinimumDate(IViewRenderer renderer, IDatePicker datePicker) => (renderer as DatePickerRenderer)?.UpdateMinimumDate(); + + void OnSelectedDateChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + if (TypedNativeView.SelectedDate.HasValue) + VirtualView.SelectedDate = TypedNativeView.SelectedDate.Value; + } + + void UpdateDate() + { + TypedNativeView.SelectedDate = VirtualView.SelectedDate; + } + + void UpdateMaximumDate() + { + TypedNativeView.DisplayDateEnd = VirtualView.MaximumDate; + } + + void UpdateMinimumDate() + { + TypedNativeView.DisplayDateStart = VirtualView.MinimumDate; + } + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.cs new file mode 100644 index 000000000000..53b73e59a1ef --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer + { + public static PropertyMapper DatePickerMapper = new PropertyMapper(LabelRenderer.ITextMapper) + { +#if __MACOS__ + [nameof(IDatePicker.SelectedDate)] = MapPropertySelectedDate, +#else + [nameof(IDatePicker.SelectedDate)] = LabelRenderer.MapPropertyText, +#endif + [nameof(IDatePicker.MaximumDate)] = MapPropertyMaximumDate, + [nameof(IDatePicker.MinimumDate)] = MapPropertyMinimumDate, + }; + + public DatePickerRenderer() : base(DatePickerMapper) + { + + } + + public DatePickerRenderer(PropertyMapper mapper) : base(mapper) + { + } + + } +} diff --git a/Maui.Core/Renderers/DatePicker/DatePickerRenderer.iOS.cs b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.iOS.cs new file mode 100644 index 000000000000..5a2d86f82b01 --- /dev/null +++ b/Maui.Core/Renderers/DatePicker/DatePickerRenderer.iOS.cs @@ -0,0 +1,84 @@ +using System.Drawing; +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class DatePickerRenderer : AbstractViewRenderer + { + UIColor _defaultTextColor; + UIDatePicker _datePicker; + + protected override PickerView CreateView() + { + var pickerView = new PickerView(); + _defaultTextColor = pickerView.TextColor; + + _datePicker = new UIDatePicker + { + Mode = UIDatePickerMode.Date, + TimeZone = new NSTimeZone("UTC"), + Date = VirtualView.SelectedDate.ToNSDate() + }; + + var width = (float)UIScreen.MainScreen.Bounds.Width; + var toolbar = new UIToolbar(new RectangleF(0, 0, width, 44)) { BarStyle = UIBarStyle.Default, Translucent = true }; + var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace); + var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => + { + VirtualView.SelectedDate = _datePicker.Date.ToDateTime(); + pickerView.ResignFirstResponder(); + }); + + toolbar.SetItems(new[] { spacer, doneButton }, false); + + _datePicker.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + toolbar.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + + pickerView.SetInputView(_datePicker); + pickerView.SetInputAccessoryView(toolbar); + + return pickerView; + } + + protected override void DisposeView(PickerView nativeView) + { + _defaultTextColor = null; + _datePicker = null; + nativeView.SetInputView(null); + nativeView.SetInputAccessoryView(null); + base.DisposeView(nativeView); + } + + public static void MapPropertyMaximumDate(IViewRenderer renderer, IDatePicker datePicker) + { + (renderer as DatePickerRenderer)?.UpdateMaximumDate(); + } + + public static void MapPropertyMinimumDate(IViewRenderer renderer, IDatePicker datePicker) + { + (renderer as DatePickerRenderer)?.UpdateMinimumDate(); + } + + + protected virtual void UpdateMaximumDate() + { + if (_datePicker == null) + { + return; + } + + _datePicker.MaximumDate = VirtualView.MaximumDate.ToNSDate(); + } + + protected virtual void UpdateMinimumDate() + { + if (_datePicker == null) + { + return; + } + + _datePicker.MinimumDate = VirtualView.MinimumDate.ToNSDate(); + } + } +} diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.Android.cs b/Maui.Core/Renderers/Editor/EditorRenderer.Android.cs new file mode 100644 index 000000000000..82963f1008cf --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.Android.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Maui.Core.Controls; +using Android.OS; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Java.Lang; + +namespace System.Maui.Platform +{ + public partial class EditorRenderer : AbstractViewRenderer + { + TextColorSwitcher _hintColorSwitcher; + TextColorSwitcher _textColorSwitcher; + + protected override MauiEditor CreateView() + { + MauiEditor mauiEditor = new MauiEditor(Context) + { + ImeOptions = ImeAction.Done + }; + + mauiEditor.SetSingleLine(false); + mauiEditor.Gravity = GravityFlags.Top; + + if ((int)Build.VERSION.SdkInt > 16) + mauiEditor.TextAlignment = TextAlignment.ViewStart; + + mauiEditor.SetHorizontallyScrolling(false); + + mauiEditor.AddTextChangedListener(new MauiTextChangedListener(VirtualView)); + + _hintColorSwitcher = new TextColorSwitcher(mauiEditor); + _textColorSwitcher = new TextColorSwitcher(mauiEditor); + + return mauiEditor; + } + + protected override void DisposeView(MauiEditor nativeView) + { + _hintColorSwitcher = null; + _textColorSwitcher = null; + + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, IEditor editor) + { + (renderer as EditorRenderer)?.UpdateTextColor(editor.Color); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is MauiEditor mauiEditor)) + return; + + if (mauiEditor.Hint == editor.Placeholder) + return; + + mauiEditor.Hint = editor.Placeholder; + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, IEditor editor) + { + (renderer as EditorRenderer)?.UpdatePlaceholderColor(editor.PlaceholderColor); + } + + public static void MapPropertyText(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is MauiEditor mauiEditor)) + return; + + string newText = editor.Text ?? string.Empty; + + if (editor.Text == newText) + return; + + newText = TrimToMaxLength(newText, editor.MaxLength); + mauiEditor.Text = newText; + mauiEditor.SetSelection(newText.Length); + } + + public static void MapPropertyMaxLenght(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is MauiEditor mauiEditor)) + return; + + var currentFilters = new List(mauiEditor?.GetFilters() ?? new IInputFilter[0]); + + for (var i = 0; i < currentFilters.Count; i++) + { + if (currentFilters[i] is InputFilterLengthFilter) + { + currentFilters.RemoveAt(i); + break; + } + } + + currentFilters.Add(new InputFilterLengthFilter(editor.MaxLength)); + + if (mauiEditor == null) + return; + + mauiEditor.SetFilters(currentFilters.ToArray()); + mauiEditor.Text = TrimToMaxLength(mauiEditor.Text, editor.MaxLength); + } + + public static void MapPropertyAutoSize(IViewRenderer renderer, IEditor editor) + { + + } + + protected virtual void UpdateTextColor(Color color) + { + _textColorSwitcher?.UpdateTextColor(color); + } + + protected virtual void UpdatePlaceholderColor(Color color) + { + _hintColorSwitcher?.UpdateHintTextColor(color); + } + + static string TrimToMaxLength(string currentText, int maxLenght) + { + if (currentText == null || currentText.Length <= maxLenght) + return currentText; + + return currentText.Substring(0, maxLenght); + } + } + + internal class MauiTextChangedListener : Java.Lang.Object, ITextWatcher + { + readonly IEditor _virtualView; + + public MauiTextChangedListener(IEditor virtualView) + { + _virtualView = virtualView; + } + + public void AfterTextChanged(IEditable s) + { + + } + + public void BeforeTextChanged(ICharSequence s, int start, int count, int after) + { + + } + + public void OnTextChanged(ICharSequence s, int start, int before, int count) + { + if (string.IsNullOrEmpty(_virtualView.Text) && s.Length() == 0) + return; + + var newText = s.ToString(); + + if (_virtualView.Text != newText) + _virtualView.Text = newText; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.Mac.cs b/Maui.Core/Renderers/Editor/EditorRenderer.Mac.cs new file mode 100644 index 000000000000..741079be369f --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.Mac.cs @@ -0,0 +1,82 @@ +using AppKit; +using Foundation; + +namespace System.Maui.Platform +{ + public partial class EditorRenderer : AbstractViewRenderer + { + const string NewLineSelector = "insertNewline"; + + protected override NSTextField CreateView() + { + var nsTextField = new NSTextField { UsesSingleLineMode = false }; + nsTextField.Cell.Scrollable = true; + nsTextField.Cell.Wraps = true; + nsTextField.DoCommandBySelector = (control, textView, commandSelector) => + { + var result = false; + if (commandSelector.Name.StartsWith(NewLineSelector, StringComparison.InvariantCultureIgnoreCase)) + { + textView.InsertText(new NSString(Environment.NewLine)); + result = true; + } + return result; + }; + + nsTextField.EditingEnded += OnEditingEnded; + nsTextField.Changed += OnChanged; + + return nsTextField; + } + + protected override void DisposeView(NSTextField nsTextField) + { + nsTextField.EditingEnded -= OnEditingEnded; + nsTextField.Changed -= OnChanged; + + base.DisposeView(nsTextField); + } + + public static void MapPropertyColor(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is NSTextField nsTexField)) + return; + + var textColor = editor.Color; + + nsTexField.TextColor = textColor.IsDefault ? NSColor.Black : textColor.ToNativeColor(); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, IEditor editor) + { + + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, IEditor editor) + { + + } + + public static void MapPropertyText(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is NSTextField nsTexField)) + return; + + if (nsTexField.StringValue != editor.Text) + nsTexField.StringValue = editor.Text ?? string.Empty; + } + + public static void MapPropertyAutoSize(IViewRenderer renderer, IEditor editor) + { + + } + + void OnEditingEnded(object sender, EventArgs eventArgs) + { + VirtualView.Completed(); + } + + void OnChanged(object sender, EventArgs e) => + VirtualView.Text = TypedNativeView.StringValue ?? string.Empty; + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.Standard.cs b/Maui.Core/Renderers/Editor/EditorRenderer.Standard.cs new file mode 100644 index 000000000000..b632637d6104 --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.Standard.cs @@ -0,0 +1,14 @@ +namespace System.Maui.Platform +{ + public partial class EditorRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyColor(IViewRenderer renderer, IEditor editor) { } + public static void MapPropertyPlaceholder(IViewRenderer renderer, IEditor editor) { } + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, IEditor editor) { } + public static void MapPropertyText(IViewRenderer renderer, IEditor editor) { } + public static void MapPropertyMaxLenght(IViewRenderer renderer, IEditor editor) { } + public static void MapPropertyAutoSize(IViewRenderer renderer, IEditor editor) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.Win32.cs b/Maui.Core/Renderers/Editor/EditorRenderer.Win32.cs new file mode 100644 index 000000000000..f3ac0e124506 --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.Win32.cs @@ -0,0 +1,106 @@ +using System.Maui.Core.Controls; +using System.Windows; +using System.Windows.Media; +using WpfScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility; +using WControl = System.Windows.Controls.Control; + +namespace System.Maui.Platform +{ + public partial class EditorRenderer : AbstractViewRenderer + { + Brush _placeholderDefaultBrush; + Brush _foregroundDefaultBrush; + + protected override MauiTextBox CreateView() + { + var control = new MauiTextBox { VerticalScrollBarVisibility = WpfScrollBarVisibility.Visible, TextWrapping = TextWrapping.Wrap, AcceptsReturn = true }; + control.LostFocus += OnTextBoxUnfocused; + control.TextChanged += OnTextBoxTextChanged; + + return control; + } + + protected override void SetupDefaults() + { + _placeholderDefaultBrush = (Brush)WControl.ForegroundProperty.GetMetadata(typeof(MauiTextBox)).DefaultValue; + _foregroundDefaultBrush = (Brush)WControl.ForegroundProperty.GetMetadata(typeof(MauiTextBox)).DefaultValue; + base.SetupDefaults(); + } + + protected override void DisposeView(MauiTextBox nativeView) + { + nativeView.LostFocus -= OnTextBoxUnfocused; + nativeView.TextChanged -= OnTextBoxTextChanged; + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, IEditor editor) => (renderer as EditorRenderer)?.UpdateTextColor(); + public static void MapPropertyPlaceholder(IViewRenderer renderer, IEditor editor) => (renderer as EditorRenderer)?.UpdatePlaceholder(); + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, IEditor editor) => (renderer as EditorRenderer)?.UpdatePlaceholderColor(); + public static void MapPropertyText(IViewRenderer renderer, IEditor editor) => (renderer as EditorRenderer)?.UpdateText(); + public static void MapPropertyMaxLenght(IViewRenderer renderer, IEditor editor) => (renderer as EditorRenderer)?.UpdateMaxLength(); + public static void MapPropertyAutoSize(IViewRenderer renderer, IEditor editor) { } + + + public virtual void UpdatePlaceholder() + { + TypedNativeView.PlaceholderText = VirtualView.Placeholder ?? string.Empty; + } + + public virtual void UpdatePlaceholderColor() + { + Color placeholderColor = VirtualView.PlaceholderColor; + + if (placeholderColor.IsDefault) + { + // Use the cached default brush + TypedNativeView.PlaceholderForegroundBrush = _placeholderDefaultBrush; + return; + } + + TypedNativeView.PlaceholderForegroundBrush = placeholderColor.ToBrush(); + } + + void OnTextBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs) + { + VirtualView.Text = TypedNativeView.Text; + } + + void OnTextBoxUnfocused(object sender, RoutedEventArgs e) + { + + } + + public virtual void UpdateText() + { + string newText = VirtualView.Text ?? ""; + + if (TypedNativeView.Text == newText) + return; + + TypedNativeView.Text = newText; + TypedNativeView.SelectionStart = TypedNativeView.Text.Length; + } + + public virtual void UpdateTextColor() + { + TypedNativeView.UpdateDependencyColor(System.Windows.Controls.Control.ForegroundProperty, VirtualView.Color); + } + + public virtual void UpdateMaxLength() + { + var maxLength = VirtualView.MaxLength; + TypedNativeView.MaxLength = maxLength; + + var currentControlText = TypedNativeView.Text; + + if (currentControlText.Length > maxLength) + TypedNativeView.Text = currentControlText.Substring(0, maxLength); + } + + public virtual void UpdateIsReadOnly() + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.cs b/Maui.Core/Renderers/Editor/EditorRenderer.cs new file mode 100644 index 000000000000..9a559e9d42c8 --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.cs @@ -0,0 +1,24 @@ +namespace System.Maui.Platform +{ + public partial class EditorRenderer + { + public static PropertyMapper EditorMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IEditor.Color)] = MapPropertyColor, + [nameof(IEditor.Text)] = MapPropertyText, + [nameof(IEditor.Placeholder)] = MapPropertyPlaceholder, + [nameof(IEditor.PlaceholderColor)] = MapPropertyPlaceholderColor, + [nameof(IEditor.AutoSize)] = MapPropertyAutoSize + }; + + public EditorRenderer() : base(EditorMapper) + { + + } + + public EditorRenderer(PropertyMapper mapper) : base(mapper ?? EditorMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/Editor/EditorRenderer.iOS.cs b/Maui.Core/Renderers/Editor/EditorRenderer.iOS.cs new file mode 100644 index 000000000000..a754304f5d32 --- /dev/null +++ b/Maui.Core/Renderers/Editor/EditorRenderer.iOS.cs @@ -0,0 +1,157 @@ +using System.Maui.Core.Controls; +using CoreGraphics; +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class EditorRenderer : AbstractViewRenderer + { + // Using same placeholder color as for the Entry + readonly UIColor _defaultPlaceholderColor = ColorExtensions.SeventyPercentGrey; + + UILabel _placeholderLabel; + + protected override MauiEditor CreateView() + { + var mauiEditor = new MauiEditor(CGRect.Empty); + + CreatePlaceholderLabel(mauiEditor); + + mauiEditor.Ended += OnEnded; + mauiEditor.Changed += OnChanged; + + return mauiEditor; + } + + protected override void DisposeView(MauiEditor mauiEditor) + { + mauiEditor.FrameChanged -= OnFrameChanged; + mauiEditor.Ended -= OnEnded; + mauiEditor.Changed -= OnChanged; + + base.DisposeView(mauiEditor); + } + + public static void MapPropertyColor(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is MauiEditor mauiEditor)) + return; + + var textColor = editor.Color; + + if (textColor.IsDefault) + mauiEditor.TextColor = UIColor.Black; + else + mauiEditor.TextColor = textColor.ToNativeColor(); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, IEditor editor) + { + if (!(renderer is EditorRenderer editorRenderer)) + return; + + editorRenderer._placeholderLabel.Text = editor.Placeholder; + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, IEditor editor) + { + if (!(renderer is EditorRenderer editorRenderer)) + return; + + Color placeholderColor = editor.PlaceholderColor; + + if (placeholderColor.IsDefault) + editorRenderer._placeholderLabel.TextColor = editorRenderer._defaultPlaceholderColor; + else + editorRenderer._placeholderLabel.TextColor = placeholderColor.ToNativeColor(); + } + + public static void MapPropertyText(IViewRenderer renderer, IEditor editor) + { + if (!(renderer is EditorRenderer editorRenderer) || !(renderer.NativeView is MauiEditor mauiEditor)) + return; + + if (mauiEditor.Text != editor.Text) + mauiEditor.Text = editor.Text; + + editorRenderer._placeholderLabel.Hidden = !string.IsNullOrEmpty(editor.Text); + } + + public static void MapPropertyMaxLenght(IViewRenderer renderer, IEditor editor) + { + if (!(renderer.NativeView is MauiEditor mauiEditor)) + return; + + var currentControlText = editor.Text; + + if (currentControlText.Length > editor.MaxLength) + mauiEditor.Text = currentControlText.Substring(0, editor.MaxLength); + } + + public static void MapPropertyAutoSize(IViewRenderer renderer, IEditor editor) + { + if (!(renderer is EditorRenderer editorRenderer) || !(renderer.NativeView is MauiEditor mauiEditor)) + return; + + mauiEditor.FrameChanged -= editorRenderer.OnFrameChanged; + if (editor.AutoSize == EditorAutoSizeOption.TextChanges) + mauiEditor.FrameChanged += editorRenderer.OnFrameChanged; + } + + void CreatePlaceholderLabel(MauiEditor control) + { + if (control == null) + { + return; + } + + _placeholderLabel = new UILabel + { + BackgroundColor = UIColor.Clear + }; + + control.AddSubview(_placeholderLabel); + + var edgeInsets = control.TextContainerInset; + var lineFragmentPadding = control.TextContainer.LineFragmentPadding; + + var vConstraints = NSLayoutConstraint.FromVisualFormat( + "V:|-" + edgeInsets.Top + "-[_placeholderLabel]-" + edgeInsets.Bottom + "-|", 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys( + new NSObject[] { _placeholderLabel }, new NSObject[] { new NSString("_placeholderLabel") }) + ); + + var hConstraints = NSLayoutConstraint.FromVisualFormat( + "H:|-" + lineFragmentPadding + "-[_placeholderLabel]-" + lineFragmentPadding + "-|", + 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys( + new NSObject[] { _placeholderLabel }, new NSObject[] { new NSString("_placeholderLabel") }) + ); + + _placeholderLabel.TranslatesAutoresizingMaskIntoConstraints = false; + + control.AddConstraints(hConstraints); + control.AddConstraints(vConstraints); + } + + void OnFrameChanged(object sender, EventArgs e) + { + // When a new line is added to the UITextView the resize happens after the view has already scrolled + // This causes the view to reposition without the scroll. If TextChanges is enabled then the Frame + // will resize until it can't anymore and thus it should never be scrolled until the Frame can't increase in size + if (VirtualView.AutoSize == EditorAutoSizeOption.TextChanges) + { + TypedNativeView.ScrollRangeToVisible(new NSRange(0, 0)); + } + } + + void OnEnded(object sender, EventArgs eventArgs) + { + VirtualView.Completed(); + } + + void OnChanged(object sender, EventArgs e) => + VirtualView.Text = TypedNativeView.Text ?? string.Empty; + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.Android.cs b/Maui.Core/Renderers/Entry/EntryRenderer.Android.cs new file mode 100644 index 000000000000..da63c72956c0 --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.Android.cs @@ -0,0 +1,104 @@ +using Android.Content; +using Android.Graphics; +using Android.Util; +using Android.Views; + +#if __ANDROID_29__ +using AndroidX.Core.View; +using AndroidX.AppCompat.Widget; +#else +using Android.Support.V4.View; +using Android.Support.V7.Widget; +#endif + +using AColor = Android.Graphics.Color; +using AView = Android.Views.View; + +namespace System.Maui.Platform +{ + public partial class EntryRenderer : AbstractViewRenderer + { + TextColorSwitcher _textColorSwitcher; + + protected override AppCompatEditText CreateView() + { + var editText = new AppCompatEditText(Context); + _textColorSwitcher = new TextColorSwitcher(editText); + + editText.TextChanged += EditTextTextChanged; + + return editText; + } + + private void EditTextTextChanged(object sender, Android.Text.TextChangedEventArgs args) => + VirtualView.Text =args.Text.ToString(); + + protected override void DisposeView(AppCompatEditText nativeView) + { + _textColorSwitcher = null; + nativeView.TextChanged -= EditTextTextChanged; + + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer.NativeView is AppCompatEditText)) + { + return; + } + + if (!(renderer is EntryRenderer entryRenderer)) + { + return; + } + + entryRenderer.UpdateTextColor(entry.Color); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer.NativeView is AppCompatEditText editText)) + { + return; + } + + editText.Hint = entry.Placeholder; + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer.NativeView is AppCompatEditText)) + { + return; + } + + if (!(renderer is EntryRenderer entryRenderer)) + { + return; + } + + entryRenderer.UpdatePlaceholderColor(entry.PlaceholderColor); + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer.NativeView is AppCompatEditText editText)) + { + return; + } + + editText.Text = entry.Text; + } + + protected virtual void UpdateTextColor(Color color) + { + _textColorSwitcher?.UpdateTextColor(color); + } + + protected virtual void UpdatePlaceholderColor(Color color) + { + _textColorSwitcher?.UpdateHintTextColor(color); + } + } +} diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.Mac.cs b/Maui.Core/Renderers/Entry/EntryRenderer.Mac.cs new file mode 100644 index 000000000000..c2dac5b3142b --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.Mac.cs @@ -0,0 +1,40 @@ + +using AppKit; + +namespace System.Maui.Platform +{ + public partial class EntryRenderer : AbstractViewRenderer + { + protected override NSTextView CreateView() + { + return new NSTextView(); + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput view) + { + var textField = renderer.NativeView as NSTextField; + + if (textField is null) + { + return; + } + + textField.SetText(view.Text); + } + } +} diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.Standard.cs b/Maui.Core/Renderers/Entry/EntryRenderer.Standard.cs new file mode 100644 index 000000000000..8ec6ffb0017d --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.Standard.cs @@ -0,0 +1,13 @@ + +namespace System.Maui.Platform +{ + public partial class EntryRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) { } + } +} diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.Win32.cs b/Maui.Core/Renderers/Entry/EntryRenderer.Win32.cs new file mode 100644 index 000000000000..746584a0bfc7 --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.Win32.cs @@ -0,0 +1,143 @@ + +using System.Maui.Core.Controls; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using static System.String; +using WControl = System.Windows.Controls.Control; + +namespace System.Maui.Platform +{ + public partial class EntryRenderer : AbstractViewRenderer + { + bool _ignoreTextChange; + Brush _placeholderDefaultBrush; + Brush _foregroundDefaultBrush; + + protected override MauiTextBox CreateView() + { + var textBox = new MauiTextBox(); + textBox.LostFocus += OnTextBoxUnfocused; + textBox.TextChanged += OnTextBoxTextChanged; + textBox.KeyUp += OnTextBoxKeyUp; + return textBox; + } + + protected override void SetupDefaults() + { + _placeholderDefaultBrush = (Brush)WControl.ForegroundProperty.GetMetadata(typeof(MauiTextBox)).DefaultValue; + _foregroundDefaultBrush = (Brush)WControl.ForegroundProperty.GetMetadata(typeof(MauiTextBox)).DefaultValue; + base.SetupDefaults(); + } + + protected override void DisposeView(MauiTextBox nativeView) + { + nativeView.LostFocus -= OnTextBoxUnfocused; + nativeView.TextChanged -= OnTextBoxTextChanged; + nativeView.KeyUp -= OnTextBoxKeyUp; + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) => (renderer as EntryRenderer)?.UpdateColor(); + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) => (renderer as EntryRenderer)?.UpdatePlaceholder(); + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) => (renderer as EntryRenderer)?.UpdatePlaceholderColor(); + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) => (renderer as EntryRenderer)?.UpdateText(); + + public virtual void UpdateColor() + { + var color = VirtualView.Color; + if (!color.IsDefault) + TypedNativeView.Foreground = color.ToBrush(); + else + TypedNativeView.Foreground = _foregroundDefaultBrush; + + // Force the PhoneTextBox control to do some internal bookkeeping + // so the colors change immediately and remain changed when the control gets focus + TypedNativeView.OnApplyTemplate(); + } + + public virtual void UpdatePlaceholder() + { + TypedNativeView.PlaceholderText = VirtualView.Placeholder ?? string.Empty; + } + + public virtual void UpdatePlaceholderColor() + { + Color placeholderColor = VirtualView.PlaceholderColor; + + if (placeholderColor.IsDefault) + { + TypedNativeView.PlaceholderForegroundBrush = _placeholderDefaultBrush; + return; + } + + TypedNativeView.PlaceholderForegroundBrush = placeholderColor.ToBrush(); + } + + public virtual void UpdateText() + { + // If the text property has changed because TextBoxOnTextChanged called SetValueFromRenderer, + // we don't want to re-update the text and reset the cursor position + if (_ignoreTextChange) + return; + + var text = VirtualView.Text; + + if (TypedNativeView.Text == text) + return; + + TypedNativeView.Text = text ?? ""; + TypedNativeView.Select(text == null ? 0 : TypedNativeView.Text.Length, 0); + } + + void UpdateMaxLength() + { + var maxLength = VirtualView.MaxLength; + TypedNativeView.MaxLength = maxLength; + + var currentControlText = TypedNativeView.Text; + + if (currentControlText.Length > maxLength) + TypedNativeView.Text = currentControlText.Substring(0, maxLength); + } + + + void OnTextBoxUnfocused(object sender, RoutedEventArgs e) + { + if (VirtualView.Color.IsDefault) + return; + + if (!IsNullOrEmpty(VirtualView.Text)) + TypedNativeView.Foreground = VirtualView.Color.ToBrush(); + } + + void OnTextBoxKeyUp(object sender, KeyEventArgs keyEventArgs) + { + + } + + void OnTextBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs) + { + // Signal to the UpdateText method that the change to TextProperty doesn't need to update the control + // This prevents the cursor position from getting lost + _ignoreTextChange = true; + VirtualView.Text = TypedNativeView.Text; + + // If an Entry.TextChanged handler modified the value of the Entry's text, the values could now be + // out-of-sync; re-sync them and fix TextBox cursor position + string entryText = VirtualView.Text; + if (TypedNativeView.Text != entryText) + { + TypedNativeView.Text = entryText; + if (TypedNativeView.Text != null) + { + var savedSelectionStart = TypedNativeView.SelectionStart; + var len = TypedNativeView.Text.Length; + TypedNativeView.SelectionStart = savedSelectionStart > len ? len : savedSelectionStart; + } + } + + _ignoreTextChange = false; + } + } +} diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.cs b/Maui.Core/Renderers/Entry/EntryRenderer.cs new file mode 100644 index 000000000000..d9ca0e41dd54 --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.cs @@ -0,0 +1,25 @@ + +namespace System.Maui.Platform +{ + public partial class EntryRenderer + { + public static PropertyMapper EntryMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IText.Color)] = MapPropertyColor, + [nameof(IText.Text)] = MapPropertyText, + + [nameof(ITextInput.Placeholder)] = MapPropertyPlaceholder, + [nameof(ITextInput.PlaceholderColor)] = MapPropertyPlaceholderColor, + }; + + public EntryRenderer() : base(EntryMapper) + { + + } + + public EntryRenderer(PropertyMapper mapper) : base(mapper ?? EntryMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/Entry/EntryRenderer.iOS.cs b/Maui.Core/Renderers/Entry/EntryRenderer.iOS.cs new file mode 100644 index 000000000000..0c8e397f94e7 --- /dev/null +++ b/Maui.Core/Renderers/Entry/EntryRenderer.iOS.cs @@ -0,0 +1,88 @@ +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class EntryRenderer : AbstractViewRenderer + { + UIColor _defaultTextColor; + + protected override UITextField CreateView() + { + var textView = new UITextField(); + _defaultTextColor = textView.TextColor; + + textView.EditingDidEnd += TextViewEditingDidEnd; + + return textView; + } + + private void TextViewEditingDidEnd(object sender, EventArgs e) => + VirtualView.Text = TypedNativeView.Text ?? string.Empty; + + protected override void DisposeView(UITextField nativeView) + { + _defaultTextColor = null; + nativeView.EditingDidEnd -= TextViewEditingDidEnd; + + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as EntryRenderer)?.UpdateTextColor(); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer is EntryRenderer entryRenderer)) + { + return; + } + + entryRenderer.UpdatePlaceholder(); + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer is EntryRenderer entryRenderer)) + { + return; + } + + entryRenderer.UpdatePlaceholder(); + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput view) + { + if (!(renderer.NativeView is UITextField textField)) + { + return; + } + + textField.Text = view.Text; + } + + protected virtual void UpdateTextColor() + { + var color = VirtualView.Color; + TypedNativeView.TextColor = color.IsDefault ? _defaultTextColor : color.ToNativeColor(); + } + + protected virtual void UpdatePlaceholder() + { + var placeholder = VirtualView.Placeholder; + + if (placeholder == null) + { + return; + } + + var targetColor = VirtualView.PlaceholderColor; + var color = targetColor.IsDefault ? ColorExtensions.SeventyPercentGrey : targetColor.ToNativeColor(); + + var attributedPlaceholder = new NSAttributedString(str: placeholder, foregroundColor: color); + TypedNativeView.AttributedPlaceholder = attributedPlaceholder; + } + } +} diff --git a/Maui.Core/Renderers/IViewRenderer.Android.cs b/Maui.Core/Renderers/IViewRenderer.Android.cs new file mode 100644 index 000000000000..d9567b3ea29b --- /dev/null +++ b/Maui.Core/Renderers/IViewRenderer.Android.cs @@ -0,0 +1,14 @@ +using System; +using AView = Android.Views.View; +using Android.Content; +using System.Maui.Core.Controls; + +namespace System.Maui { + public interface IAndroidViewRenderer : IViewRenderer + { + + void SetContext(Context context); + + AView View { get; } + } +} diff --git a/Maui.Core/Renderers/IViewRenderer.Mac.cs b/Maui.Core/Renderers/IViewRenderer.Mac.cs new file mode 100644 index 000000000000..3332cf93a928 --- /dev/null +++ b/Maui.Core/Renderers/IViewRenderer.Mac.cs @@ -0,0 +1,9 @@ +using AppKit; + +namespace System.Maui +{ + public interface INativeViewRenderer : IViewRenderer + { + NSView View { get; } + } +} diff --git a/Maui.Core/Renderers/IViewRenderer.cs b/Maui.Core/Renderers/IViewRenderer.cs new file mode 100644 index 000000000000..fc0a3f9006b7 --- /dev/null +++ b/Maui.Core/Renderers/IViewRenderer.cs @@ -0,0 +1,17 @@ +using System; +using System.Maui.Core; +using System.Maui.Core.Controls; + +namespace System.Maui { + public interface IViewRenderer + { + void SetView (IFrameworkElement view); + void UpdateValue (string property); + void Remove (IFrameworkElement view); + object NativeView { get; } + bool HasContainer { get; set; } + ContainerView ContainerView { get; } + SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint); + void SetFrame (Rectangle frame); + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/IViewRenderer.iOS.cs b/Maui.Core/Renderers/IViewRenderer.iOS.cs new file mode 100644 index 000000000000..25ba36a2cfc8 --- /dev/null +++ b/Maui.Core/Renderers/IViewRenderer.iOS.cs @@ -0,0 +1,11 @@ +using System; +using System.Maui.Core.Controls; +using UIKit; + +namespace System.Maui { + public interface INativeViewRenderer : IViewRenderer { + + UIView View { get; } + + } +} diff --git a/Maui.Core/Renderers/Label/LabelRenderer.Android.cs b/Maui.Core/Renderers/Label/LabelRenderer.Android.cs new file mode 100644 index 000000000000..a69fd5d62c23 --- /dev/null +++ b/Maui.Core/Renderers/Label/LabelRenderer.Android.cs @@ -0,0 +1,129 @@ +using System; +using Android.Content; + +using System.ComponentModel; +using Android.Content.Res; +using Android.Graphics; +#if __ANDROID_29__ +using AndroidX.Core.View; +#else +using Android.Support.V4.View; +#endif +using Android.Text; +using Android.Views; +using Android.Widget; + +namespace System.Maui.Platform +{ + public partial class LabelRenderer : AbstractViewRenderer + { + ColorStateList _labelTextColorDefault; + Color _lastUpdateColor = Color.Default; + float _lineSpacingExtraDefault = -1.0f; + float _lineSpacingMultiplierDefault = -1.0f; + + int _lastConstraintHeight; + int _lastConstraintWidth; + SizeRequest? _lastSizeRequest; + + protected override TextView CreateView() + { + var text = new TextView(Context); + + // Doing this temporarily to make the Label sensible; letting it do multiple lines + // is causing it to explode all over the page. We'll figure that out later. + text.SetMaxLines(1); + + _labelTextColorDefault = text.TextColors; + return text; + } + + public static void MapPropertyText(IViewRenderer renderer, IText view) + { + var textView = renderer.NativeView as TextView; + if (textView == null) + return; + //TODO: lets make a way for any renderer to tell it to reset last cached size request + if(renderer is LabelRenderer lr) + lr._lastSizeRequest = null; + textView.Text = view.Text; + + view.InvalidateMeasure(); + textView.RequestLayout(); + } + + public static void MapPropertyColor(IViewRenderer renderer, IText view) + { + var labelRenderer = renderer as LabelRenderer; + if (labelRenderer == null) + return; + var c = view.Color; + if (c == labelRenderer._lastUpdateColor) + return; + + + if (c.IsDefault) + labelRenderer.TypedNativeView.SetTextColor(labelRenderer._labelTextColorDefault); + else + labelRenderer.TypedNativeView.SetTextColor(c.ToNative()); + } + + public static void MapPropertyLineHeight(IViewRenderer renderer, ILabel view) + { + var nativeLabel = renderer.NativeView as TextView; + var labelRenderer = renderer as LabelRenderer; + if (labelRenderer._lineSpacingExtraDefault < 0) + labelRenderer._lineSpacingExtraDefault = nativeLabel.LineSpacingExtra; + if (labelRenderer._lineSpacingMultiplierDefault < 0) + labelRenderer._lineSpacingMultiplierDefault = nativeLabel.LineSpacingMultiplier; + + if (view.LineHeight == -1) + nativeLabel.SetLineSpacing(labelRenderer._lineSpacingExtraDefault, labelRenderer._lineSpacingMultiplierDefault); + else if (nativeLabel.LineHeight >= 0) + nativeLabel.SetLineSpacing(0, (float)nativeLabel.LineHeight); + + labelRenderer._lastSizeRequest = null; + } + + public override SizeRequest GetDesiredSize(double wConstraint, double hConstraint) + { + int widthConstraint = wConstraint == double.MaxValue ? int.MaxValue : (int)wConstraint; + int heightConstraint = hConstraint == double.MaxValue ? int.MaxValue : (int)hConstraint; + var hint = TypedNativeView.Hint; + if (!string.IsNullOrEmpty(hint)) + TypedNativeView.Hint = string.Empty; + + var deviceWidthConstraint = Context.ToPixels(widthConstraint); + var deviceHeightConstraint = Context.ToPixels(heightConstraint); + + var widthSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceWidthConstraint); + var heightSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceHeightConstraint); + + TypedNativeView.Measure(widthSpec, heightSpec); + + var deviceIndependentSize = Context.FromPixels(TypedNativeView.MeasuredWidth, TypedNativeView.MeasuredHeight); + + var result = new SizeRequest(deviceIndependentSize, new Size()); + + //Set Hint back after sizing + TypedNativeView.Hint = hint; + + result.Minimum = new Size(Math.Min(Context.ToPixels(10), result.Request.Width), result.Request.Height); + + // if the measure of the view has changed then trigger a request for layout + // if the measure hasn't changed then force a layout of the label + var measureIsChanged = !_lastSizeRequest.HasValue || + _lastSizeRequest.HasValue && (_lastSizeRequest.Value.Request.Height != TypedNativeView.MeasuredHeight || _lastSizeRequest.Value.Request.Width != TypedNativeView.MeasuredWidth); + if (measureIsChanged) + TypedNativeView.RequestLayout(); + else + TypedNativeView.ForceLayout(); + + _lastConstraintWidth = (int)widthConstraint; + _lastConstraintHeight = (int)heightConstraint; + _lastSizeRequest = result; + + return result; + } + } +} diff --git a/Maui.Core/Renderers/Label/LabelRenderer.MaciOS.cs b/Maui.Core/Renderers/Label/LabelRenderer.MaciOS.cs new file mode 100644 index 000000000000..f4b2e4dcb5c3 --- /dev/null +++ b/Maui.Core/Renderers/Label/LabelRenderer.MaciOS.cs @@ -0,0 +1,78 @@ + +using System; +using System.ComponentModel; +using RectangleF = CoreGraphics.CGRect; +using SizeF = CoreGraphics.CGSize; +using Foundation; +using System.Collections.Generic; +using CoreGraphics; +using System.Diagnostics; +using System.Maui.Core; + +#if __MOBILE__ +using NativeLabel = UIKit.UILabel; +#else +using NativeLabel = AppKit.NSTextField; +#endif + + +namespace System.Maui.Platform +{ + public partial class LabelRenderer : AbstractViewRenderer + { + //static Color? DefaultTextColor; + + protected override NativeLabel CreateView() + { + var label = new NativeLabel(); + //if (DefaultTextColor == null) + // DefaultTextColor = label.TextColor.ToColor(); + return label; + } + + + public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + var result = base.GetDesiredSize(widthConstraint, heightConstraint); + var tinyWidth = Math.Min(10, result.Request.Width); + result.Minimum = new Size(tinyWidth, result.Request.Height); + + return result; + } + + + public static void MapPropertyText(IViewRenderer renderer, IText view) + { + var label = renderer.NativeView as NativeLabel; + if(label == null) + return; + + if (view.TextType == TextType.Html) + { + + label.SetText(view.Text.ToNSAttributedString()); + } + else + { + label.SetText(view.Text); + } + + view.InvalidateMeasure(); +#if __MOBILE__ + label.Superview?.SetNeedsLayout(); +#endif + } + public static void MapPropertyColor (IViewRenderer renderer, IText view) { + var label = renderer.NativeView as NativeLabel; + if (label == null) + return; + label.TextColor = view.Color.ToNativeColor(); + } + + + public static void MapPropertyLineHeight(IViewRenderer renderer, ILabel view) + { + + } + } +} diff --git a/Maui.Core/Renderers/Label/LabelRenderer.Standard.cs b/Maui.Core/Renderers/Label/LabelRenderer.Standard.cs new file mode 100644 index 000000000000..695f81a4a454 --- /dev/null +++ b/Maui.Core/Renderers/Label/LabelRenderer.Standard.cs @@ -0,0 +1,11 @@ +using System; +namespace System.Maui.Platform +{ + public partial class LabelRenderer : AbstractViewRenderer + { + public static void MapPropertyText(IViewRenderer renderer, IText view) { } + public static void MapPropertyColor(IViewRenderer renderer, IText view) { } + public static void MapPropertyLineHeight(IViewRenderer renderer, ILabel view) { } + protected override object CreateView () => throw new NotImplementedException (); + } +} diff --git a/Maui.Core/Renderers/Label/LabelRenderer.Win32.cs b/Maui.Core/Renderers/Label/LabelRenderer.Win32.cs new file mode 100644 index 000000000000..dea63054420a --- /dev/null +++ b/Maui.Core/Renderers/Label/LabelRenderer.Win32.cs @@ -0,0 +1,25 @@ +using System.Windows.Controls; +using System.Windows.Media; + +namespace System.Maui.Platform +{ + public partial class LabelRenderer : AbstractViewRenderer + { + protected override TextBlock CreateView() => new TextBlock(); + + public static void MapPropertyText(IViewRenderer renderer, IText view) => (renderer as LabelRenderer)?.UpdateText(); + public static void MapPropertyColor(IViewRenderer renderer, IText view) => (renderer as LabelRenderer)?.UpdateColor(); + public static void MapPropertyLineHeight(IViewRenderer renderer, ILabel view) { } + + public virtual void UpdateText() + { + TypedNativeView.Text = VirtualView.Text; + } + + public virtual void UpdateColor() + { + var textColor = VirtualView.Color; + TypedNativeView.Foreground = !textColor.IsDefault ? textColor.ToBrush() : Brushes.Black; + } + } +} diff --git a/Maui.Core/Renderers/Label/LabelRenderer.cs b/Maui.Core/Renderers/Label/LabelRenderer.cs new file mode 100644 index 000000000000..e491d282f397 --- /dev/null +++ b/Maui.Core/Renderers/Label/LabelRenderer.cs @@ -0,0 +1,27 @@ +using System; + +namespace System.Maui.Platform { + public partial class LabelRenderer { + public static PropertyMapper ITextMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(ILabel.Text)] = MapPropertyText, + [nameof(ILabel.Color)] = MapPropertyColor, + }; + + public static PropertyMapper Mapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(ILabel.Text)] = MapPropertyText, + [nameof(ILabel.Color)] = MapPropertyColor, + [nameof(ILabel.LineHeight)] = MapPropertyLineHeight, + }; + + public LabelRenderer () : base (Mapper) + { + + } + public LabelRenderer (PropertyMapper mapper) : base (mapper ?? Mapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.Android.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.Android.cs new file mode 100644 index 000000000000..787eee3cf17b --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.Android.cs @@ -0,0 +1,39 @@ +namespace System.Maui.Platform +{ + public partial class LayoutRenderer : AbstractViewRenderer + { + protected override LayoutViewGroup CreateView() + { + var viewGroup = new LayoutViewGroup(Context) + { + CrossPlatformMeasure = VirtualView.Measure, + CrossPlatformArrange = VirtualView.Arrange + }; + + return viewGroup; + } + + public override void SetView(IFrameworkElement view) + { + base.SetView(view); + + TypedNativeView.CrossPlatformMeasure = VirtualView.Measure; + TypedNativeView.CrossPlatformArrange = VirtualView.Arrange; + + foreach (var child in VirtualView.Children) + { + TypedNativeView.AddView(child.ToNative(Context)); + } + } + + protected override void DisposeView(LayoutViewGroup nativeView) + { + nativeView.CrossPlatformArrange = null; + nativeView.CrossPlatformMeasure = null; + + nativeView.RemoveAllViews(); + + base.DisposeView(nativeView); + } + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.Mac.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.Mac.cs new file mode 100644 index 000000000000..3c28ddbf3099 --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.Mac.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using AppKit; + +namespace System.Maui.Platform +{ + public partial class LayoutRenderer : AbstractViewRenderer + { + protected override NSView CreateView() + { + return new NSView(); + } + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.Standard.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.Standard.cs new file mode 100644 index 000000000000..a37b25a010c5 --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.Standard.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class LayoutRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.Win32.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.Win32.cs new file mode 100644 index 000000000000..2f799f650c99 --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.Win32.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +namespace System.Maui.Platform +{ + partial class LayoutRenderer : AbstractViewRenderer + { + protected override LayoutPanel CreateView() + { + return new LayoutPanel(); + } + + public override void SetView(IFrameworkElement view) + { + base.SetView(view); + + TypedNativeView.CrossPlatformMeasure = VirtualView.Measure; + TypedNativeView.CrossPlatformArrange = VirtualView.Arrange; + + foreach (var child in VirtualView.Children) + { + TypedNativeView.Children.Add(child.ToNative()); + } + } + + protected override void DisposeView(LayoutPanel nativeView) + { + nativeView.CrossPlatformArrange = null; + nativeView.CrossPlatformMeasure = null; + + nativeView.Children.Clear(); + + base.DisposeView(nativeView); + } + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.cs new file mode 100644 index 000000000000..b4e6684bc055 --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class LayoutRenderer + { + public static PropertyMapper LayoutMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + // Maps here + }; + + public LayoutRenderer() : base(LayoutMapper) + { + + } + + public LayoutRenderer(PropertyMapper mapper) : base(mapper ?? LayoutMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/Layout/LayoutRenderer.iOS.cs b/Maui.Core/Renderers/Layout/LayoutRenderer.iOS.cs new file mode 100644 index 000000000000..b7eda931ed28 --- /dev/null +++ b/Maui.Core/Renderers/Layout/LayoutRenderer.iOS.cs @@ -0,0 +1,43 @@ +using System; +namespace System.Maui.Platform +{ + public partial class LayoutRenderer : AbstractViewRenderer + { + protected override LayoutView CreateView() + { + var view = new LayoutView + { + CrossPlatformMeasure = VirtualView.Measure, + CrossPlatformArrange = VirtualView.Arrange, + }; + + return view; + } + + public override void SetView(IFrameworkElement view) + { + base.SetView(view); + + TypedNativeView.CrossPlatformMeasure = VirtualView.Measure; + TypedNativeView.CrossPlatformArrange = VirtualView.Arrange; + + foreach (var child in VirtualView.Children) + { + TypedNativeView.AddSubview(child.ToNative()); + } + } + + protected override void DisposeView(LayoutView nativeView) + { + nativeView.CrossPlatformArrange = null; + nativeView.CrossPlatformMeasure = null; + + foreach (var subview in nativeView.Subviews) + { + subview.RemoveFromSuperview(); + } + + base.DisposeView(nativeView); + } + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.Android.cs b/Maui.Core/Renderers/Page/PageRenderer.Android.cs new file mode 100644 index 000000000000..2c5b86520fee --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.Android.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Maui.Platform; +using System.Text; +using Android.Views; +using AView = Android.Views.View; + +namespace System.Maui.Platform +{ + public partial class PageRenderer + { + protected override AView CreateView() + { + LayoutInflater inflater = LayoutInflater.FromContext(this.Context); + var view = inflater.Inflate(Resource.Layout.content_main, null); + + if(view is ViewGroup vg && base.VirtualView.Content is IFrameworkElement fe) + { + vg.AddView(fe.ToNative(Context)); + } + + return view; + } + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.Mac.cs b/Maui.Core/Renderers/Page/PageRenderer.Mac.cs new file mode 100644 index 000000000000..eb4750746b3c --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.Mac.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Maui.Platform; +using System.Text; +using AppKit; + +namespace System.Maui.Platform +{ + public partial class PageRenderer + { + protected override NSView CreateView() + { + throw new NotImplementedException(); + } + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.Standard.cs b/Maui.Core/Renderers/Page/PageRenderer.Standard.cs new file mode 100644 index 000000000000..d5c65680526e --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.Standard.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class PageRenderer + { + protected override object CreateView() + { + throw new NotImplementedException(); + } + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.Win32.cs b/Maui.Core/Renderers/Page/PageRenderer.Win32.cs new file mode 100644 index 000000000000..e209af173c1f --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.Win32.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Maui.Platform; +using System.Text; +using System.Windows.Controls; + +namespace System.Maui.Platform +{ + public partial class PageRenderer + { + protected override UserControl CreateView() + { + throw new NotImplementedException(); + } + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.cs b/Maui.Core/Renderers/Page/PageRenderer.cs new file mode 100644 index 000000000000..f5f407669e80 --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +#if __IOS__ +using NativeView = UIKit.UIView; +#elif __MACOS__ +using NativeView = AppKit.NSView; +#elif MONOANDROID +using NativeView = Android.Views.View; +#elif NETCOREAPP +using NativeView = System.Windows.Controls.UserControl; +#else +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform +{ + public partial class PageRenderer : AbstractViewRenderer + { + public static PropertyMapper PageRendererMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + }; + + public PageRenderer() : base(PageRendererMapper) + { + + } + + public PageRenderer(PropertyMapper mapper) : base(mapper) + { + } + + } +} diff --git a/Maui.Core/Renderers/Page/PageRenderer.iOS.cs b/Maui.Core/Renderers/Page/PageRenderer.iOS.cs new file mode 100644 index 000000000000..263a2349f421 --- /dev/null +++ b/Maui.Core/Renderers/Page/PageRenderer.iOS.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Maui.Platform; +using System.Text; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class PageRenderer + { + protected override UIView CreateView() + { + throw new NotImplementedException(); + } + } +} diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.Android.cs b/Maui.Core/Renderers/Picker/PickerRenderer.Android.cs new file mode 100644 index 000000000000..72e8cb57cf7e --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.Android.cs @@ -0,0 +1,135 @@ +using System.Collections.Specialized; +using System.Linq; +using System.Maui.Core.Controls; +using Android.App; +using Android.Text; +using Android.Text.Style; + +namespace System.Maui.Platform +{ + public partial class PickerRenderer : AbstractViewRenderer + { + AlertDialog _dialog; + TextColorSwitcher _textColorSwitcher; + TextColorSwitcher _hintColorSwitcher; + + protected override MauiPicker CreateView() + { + var mauiPicker = new MauiPicker(Context); + + mauiPicker.Click += OnClick; + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged += OnCollectionChanged; + + return mauiPicker; + } + + protected override void DisposeView(MauiPicker mauiPickerText) + { + mauiPickerText.Click -= OnClick; + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged -= OnCollectionChanged; + + base.DisposeView(mauiPickerText); + } + + public static void MapPropertyTitle(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + public static void MapPropertyTitleColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateTitleColor(); + } + + public static void MapPropertyTextColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateTextColor(); + } + + public static void MapPropertySelectedIndex(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + void UpdatePicker() + { + if (TypedNativeView == null || VirtualView == null) + return; + + UpdateTitleColor(); + + if (VirtualView.SelectedIndex == -1 || VirtualView.Items == null || VirtualView.SelectedIndex >= VirtualView.Items.Count) + TypedNativeView.Text = null; + else + TypedNativeView.Text = VirtualView.Items[VirtualView.SelectedIndex]; + } + + void UpdateTitleColor() + { + if (TypedNativeView == null || VirtualView == null) + return; + + _hintColorSwitcher = _hintColorSwitcher ?? new TextColorSwitcher(TypedNativeView); + _hintColorSwitcher.UpdateTextColor(VirtualView.TitleColor); + } + + void UpdateTextColor() + { + if (TypedNativeView == null || VirtualView == null) + return; + + _textColorSwitcher = _textColorSwitcher ?? new TextColorSwitcher(TypedNativeView); + _textColorSwitcher.UpdateTextColor(VirtualView.TextColor); + } + + void OnClick(object sender, EventArgs e) + { + if (VirtualView == null) + return; + + if (_dialog == null) + { + using (var builder = new AlertDialog.Builder(Context)) + { + if (VirtualView.TitleColor == Color.Default) + { + builder.SetTitle(VirtualView.Title ?? string.Empty); + } + else + { + var title = new SpannableString(VirtualView.Title ?? string.Empty); + title.SetSpan(new ForegroundColorSpan(VirtualView.TitleColor.ToNative()), 0, title.Length(), SpanTypes.ExclusiveExclusive); + builder.SetTitle(title); + } + + string[] items = VirtualView.Items.ToArray(); + builder.SetItems(items, (s, e) => + { + var selectedIndex = e.Which; + VirtualView.SelectedIndex = selectedIndex; + UpdatePicker(); + }); + + builder.SetNegativeButton(Android.Resource.String.Cancel, (o, args) => { }); + + _dialog = builder.Create(); + } + + _dialog.SetCanceledOnTouchOutside(true); + + _dialog.DismissEvent += (sender, args) => + { + _dialog.Dispose(); + _dialog = null; + }; + + _dialog.Show(); + } + } + + void OnCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.Mac.cs b/Maui.Core/Renderers/Picker/PickerRenderer.Mac.cs new file mode 100644 index 000000000000..33468ad8df3f --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.Mac.cs @@ -0,0 +1,96 @@ +using System.Collections.Specialized; +using System.Linq; +using AppKit; +using Foundation; + +namespace System.Maui.Platform +{ + public partial class PickerRenderer : AbstractViewRenderer + { + protected override NSPopUpButton CreateView() + { + var nSPopUpButton = new NSPopUpButton(); + + nSPopUpButton.Activated -= ComboBoxSelectionChanged; + nSPopUpButton.Activated += ComboBoxSelectionChanged; + + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged -= RowsCollectionChanged; + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged += RowsCollectionChanged; + + return nSPopUpButton; + } + protected override void DisposeView(NSPopUpButton nSPopUpButton) + { + nSPopUpButton.Activated -= ComboBoxSelectionChanged; + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged -= RowsCollectionChanged; + + base.DisposeView(nSPopUpButton); + } + + public static void MapPropertyTitle(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + public static void MapPropertyTitleColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + public static void MapPropertyTextColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateTextColor(); + } + + public static void MapPropertySelectedIndex(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + public virtual void UpdatePicker() + { + if (TypedNativeView == null || VirtualView == null) + return; + + var items = VirtualView.Items; + + TypedNativeView.RemoveAllItems(); + TypedNativeView.AddItems(items.ToArray()); + + var selectedIndex = VirtualView.SelectedIndex; + + if (items == null || items.Count == 0 || selectedIndex < 0) + return; + + TypedNativeView.SelectItem(selectedIndex); + } + + public virtual void UpdateTextColor() + { + if (TypedNativeView == null || VirtualView == null) + return; + + foreach (NSMenuItem it in TypedNativeView.Items()) + { + it.AttributedTitle = new NSAttributedString(); + } + + var color = VirtualView.Color; + if (color != Color.Default && TypedNativeView.SelectedItem != null) + { + NSAttributedString textWithColor = new NSAttributedString(TypedNativeView.SelectedItem.Title, foregroundColor: color.ToNativeColor(), paragraphStyle: new NSMutableParagraphStyle() { Alignment = NSTextAlignment.Left }); + TypedNativeView.SelectedItem.AttributedTitle = textWithColor; + } + } + + void RowsCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + + void ComboBoxSelectionChanged(object sender, EventArgs e) + { + VirtualView.SelectedIndex = (int)TypedNativeView.IndexOfSelectedItem; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.Standard.cs b/Maui.Core/Renderers/Picker/PickerRenderer.Standard.cs new file mode 100644 index 000000000000..837aacc63a6f --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.Standard.cs @@ -0,0 +1,12 @@ +namespace System.Maui.Platform +{ + public partial class PickerRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyTitle(IViewRenderer renderer, IPicker picker) { } + public static void MapPropertyTitleColor(IViewRenderer renderer, IPicker picker) { } + public static void MapPropertyTextColor(IViewRenderer renderer, IPicker picker) { } + public static void MapPropertySelectedIndex(IViewRenderer renderer, IPicker picker) { } + } +} diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.Win32.cs b/Maui.Core/Renderers/Picker/PickerRenderer.Win32.cs new file mode 100644 index 000000000000..b3a46829236d --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.Win32.cs @@ -0,0 +1,73 @@ +using System.Windows.Controls; +using WSelectionChangedEventArgs = System.Windows.Controls.SelectionChangedEventArgs; + +namespace System.Maui.Platform +{ + public partial class PickerRenderer : AbstractViewRenderer + { + const string TextBoxTemplate = "PART_EditableTextBox"; + protected override ComboBox CreateView() + { + var combox = new ComboBox(); + combox.IsEditable = true; + combox.SelectionChanged += OnControlSelectionChanged; + combox.Loaded += OnControlLoaded; + + combox.ItemsSource = ((LockableObservableListWrapper)VirtualView.Items)._list; + return combox; + } + + protected override void DisposeView(ComboBox nativeView) + { + nativeView.SelectionChanged -= OnControlSelectionChanged; + nativeView.Loaded -= OnControlLoaded; + + base.DisposeView(nativeView); + } + + public static void MapPropertyTitle(IViewRenderer renderer, IPicker picker) => (renderer as PickerRenderer)?.UpdateTitle(); + public static void MapPropertyTitleColor(IViewRenderer renderer, IPicker picker) => (renderer as PickerRenderer)?.UpdateTitleColor(); + public static void MapPropertyTextColor(IViewRenderer renderer, IPicker picker) => (renderer as PickerRenderer)?.UpdateTextColor(); + public static void MapPropertySelectedIndex(IViewRenderer renderer, IPicker picker) => (renderer as PickerRenderer)?.UpdateSelectedIndex(); + + public virtual void UpdateBackgroundColor() + { + var textbox = (TextBox)TypedNativeView.Template.FindName(TextBoxTemplate, TypedNativeView); + if (textbox != null) + { + var parent = (Border)textbox.Parent; + parent.Background = VirtualView.BackgroundColor.ToBrush(); + } + } + + public virtual void UpdateSelectedIndex() + { + TypedNativeView.SelectedIndex = VirtualView.SelectedIndex; + } + + public virtual void UpdateTitle() + { + //TODO: Create full size combobox + } + + public virtual void UpdateTitleColor() + { + //TODO: Create full size combobox + } + + public virtual void UpdateTextColor() + { + TypedNativeView.UpdateDependencyColor(ComboBox.ForegroundProperty, VirtualView.TextColor); + } + + void OnControlSelectionChanged(object sender, WSelectionChangedEventArgs e) + { + VirtualView.SelectedIndex = TypedNativeView.SelectedIndex; + } + + void OnControlLoaded(object sender, System.Windows.RoutedEventArgs e) + { + UpdateBackgroundColor(); + } + } +} diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.cs b/Maui.Core/Renderers/Picker/PickerRenderer.cs new file mode 100644 index 000000000000..126e0af5544d --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.cs @@ -0,0 +1,23 @@ +namespace System.Maui.Platform +{ + public partial class PickerRenderer + { + public static PropertyMapper PickerMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IPicker.Title)] = MapPropertyTitle, + [nameof(IPicker.TitleColor)] = MapPropertyTitleColor, + [nameof(IPicker.TextColor)] = MapPropertyTextColor, + [nameof(IPicker.SelectedIndex)] = MapPropertySelectedIndex + }; + + public PickerRenderer() : base(PickerMapper) + { + + } + + public PickerRenderer(PropertyMapper mapper) : base(mapper) + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Picker/PickerRenderer.iOS.cs b/Maui.Core/Renderers/Picker/PickerRenderer.iOS.cs new file mode 100644 index 000000000000..c3bc70beb7a9 --- /dev/null +++ b/Maui.Core/Renderers/Picker/PickerRenderer.iOS.cs @@ -0,0 +1,247 @@ +using System.Collections.Specialized; +using System.Maui.Core.Controls; +using Foundation; +using UIKit; +using RectangleF = CoreGraphics.CGRect; + +namespace System.Maui.Platform +{ + public partial class PickerRenderer : AbstractViewRenderer + { + UIPickerView _picker; + UIColor _defaultTextColor; + + protected override MauiPicker CreateView() + { + var mauiPicker = new MauiPicker { BorderStyle = UITextBorderStyle.RoundedRect }; + + mauiPicker.EditingChanged += OnEditing; + + _picker = new UIPickerView(); + + var width = UIScreen.MainScreen.Bounds.Width; + var toolbar = new UIToolbar(new RectangleF(0, 0, width, 44)) { BarStyle = UIBarStyle.Default, Translucent = true }; + var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace); + + var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => + { + var pickerSource = (PickerSource)_picker.Model; + + if (VirtualView.SelectedIndex == -1 && VirtualView.Items != null && VirtualView.Items.Count > 0) + UpdateSelectedIndex(0); + + mauiPicker.Text = pickerSource.SelectedItem; + mauiPicker.ResignFirstResponder(); + }); + + toolbar.SetItems(new[] { spacer, doneButton }, false); + + mauiPicker.InputView = _picker; + mauiPicker.InputAccessoryView = toolbar; + + mauiPicker.InputView.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + mauiPicker.InputAccessoryView.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + + if (UIDevice.CurrentDevice.CheckSystemVersion(9, 0)) + { + mauiPicker.InputAssistantItem.LeadingBarButtonGroups = null; + mauiPicker.InputAssistantItem.TrailingBarButtonGroups = null; + } + + _defaultTextColor = mauiPicker.TextColor; + + mauiPicker.AccessibilityTraits = UIAccessibilityTrait.Button; + + _picker.Model = new PickerSource(VirtualView); + + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged += OnCollectionChanged; + + return mauiPicker; + } + + protected override void DisposeView(MauiPicker mauiPicker) + { + _defaultTextColor = null; + + if (_picker != null) + { + if (_picker.Model != null) + { + _picker.Model.Dispose(); + _picker.Model = null; + } + + _picker.RemoveFromSuperview(); + _picker.Dispose(); + _picker = null; + } + + mauiPicker.EditingChanged -= OnEditing; + + ((INotifyCollectionChanged)VirtualView.Items).CollectionChanged -= OnCollectionChanged; + + base.DisposeView(mauiPicker); + } + + public static void MapPropertyTitle(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdatePicker(); + } + + public static void MapPropertyTitleColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateTitleColor(); + } + + public static void MapPropertyTextColor(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateTextColor(); + } + + public static void MapPropertySelectedIndex(IViewRenderer renderer, IPicker picker) + { + (renderer as PickerRenderer)?.UpdateSelectedIndex(picker.SelectedIndex); + } + + void UpdatePicker() + { + var selectedIndex = VirtualView.SelectedIndex; + var items = VirtualView.Items; + + UpdateTitleColor(); + + TypedNativeView.Text = selectedIndex == -1 || items == null || selectedIndex >= items.Count ? string.Empty : items[selectedIndex]; + _picker.ReloadAllComponents(); + + if (items == null || items.Count == 0) + return; + + UpdateSelectedIndex(selectedIndex); + } + + void UpdateTextColor() + { + if (VirtualView == null || TypedNativeView == null) + return; + + var textColor = VirtualView.Color; + + if (textColor.IsDefault || (!VirtualView.IsEnabled)) + TypedNativeView.TextColor = _defaultTextColor; + else + TypedNativeView.TextColor = textColor.ToNativeColor(); + + // HACK This forces the color to update; there's probably a more elegant way to make this happen + TypedNativeView.Text = TypedNativeView.Text; + } + + void UpdateSelectedIndex(int selectedIndex) + { + if (VirtualView == null) + return; + + VirtualView.SelectedIndex = selectedIndex; + + var source = (PickerSource)_picker.Model; + source.SelectedIndex = selectedIndex; + source.SelectedItem = selectedIndex >= 0 ? VirtualView.Items[selectedIndex] : null; + _picker.Select(Math.Max(selectedIndex, 0), 0, true); + } + + void UpdateTitleColor() + { + if (VirtualView == null) + return; + + var title = VirtualView.Title; + + if (string.IsNullOrEmpty(title)) + return; + + var titleColor = VirtualView.TitleColor; + + UpdateAttributedPlaceholder(new NSAttributedString(title, null, titleColor.ToNativeColor())); + } + + void UpdateAttributedPlaceholder(NSAttributedString nsAttributedString) + { + if (TypedNativeView == null) + return; + + TypedNativeView.AttributedPlaceholder = nsAttributedString; + } + + void OnCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + + void OnEditing(object sender, EventArgs eventArgs) + { + // Reset the TextField's Text so it appears as if typing with a keyboard does not work. + var selectedIndex = VirtualView.SelectedIndex; + var items = VirtualView.Items; + TypedNativeView.Text = selectedIndex == -1 || items == null ? "" : items[selectedIndex]; + + // Also clears the undo stack (undo/redo possible on iPads) + TypedNativeView.UndoManager.RemoveAllActions(); + } + } + + class PickerSource : UIPickerViewModel + { + IPicker _virtualView; + bool _disposed; + + public PickerSource(IPicker virtualView) + { + _virtualView = virtualView; + } + + public int SelectedIndex { get; internal set; } + + public string SelectedItem { get; internal set; } + + public override nint GetComponentCount(UIPickerView picker) + { + return 1; + } + + public override nint GetRowsInComponent(UIPickerView pickerView, nint component) + { + return _virtualView.Items != null ? _virtualView.Items.Count : 0; + } + + public override string GetTitle(UIPickerView picker, nint row, nint component) + { + return _virtualView.Items[(int)row]; + } + + public override void Selected(UIPickerView picker, nint row, nint component) + { + if (_virtualView.Items.Count == 0) + { + SelectedItem = null; + SelectedIndex = -1; + } + else + { + SelectedItem = _virtualView.Items[(int)row]; + SelectedIndex = (int)row; + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + _virtualView = null; + + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Android.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Android.cs new file mode 100644 index 000000000000..8cfc959756fd --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Android.cs @@ -0,0 +1,53 @@ +using Android.Content.Res; +using Android.OS; +using AProgressBar = Android.Widget.ProgressBar; + +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer : AbstractViewRenderer + { + protected override AProgressBar CreateView() + { + return new AProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleHorizontal) { Indeterminate = false, Max = 10000 }; + } + + public static void MapPropertyProgress(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is AProgressBar aProgressBar)) + return; + + aProgressBar.Progress = (int)(progressBar.Progress * 10000); + } + + public static void MapPropertyProgressColor(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is AProgressBar aProgressBar)) + return; + + + Color color = progressBar.ProgressColor; + + if (color.IsDefault) + { + (aProgressBar.Indeterminate ? aProgressBar.IndeterminateDrawable : + aProgressBar.ProgressDrawable).ClearColorFilter(); + } + else + { + if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop) + { + (aProgressBar.Indeterminate ? aProgressBar.IndeterminateDrawable : + aProgressBar.ProgressDrawable).SetColorFilter(color, FilterMode.SrcIn); + } + else + { + var tintList = ColorStateList.ValueOf(color.ToNative()); + if (aProgressBar.Indeterminate) + aProgressBar.IndeterminateTintList = tintList; + else + aProgressBar.ProgressTintList = tintList; + } + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Mac.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Mac.cs new file mode 100644 index 000000000000..f5fb59f4d358 --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Mac.cs @@ -0,0 +1,65 @@ +using System.Linq; +using AppKit; +using CoreImage; + +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer : AbstractViewRenderer + { + static CIColorPolynomial _currentColorFilter; + static NSColor _currentColor; + + protected override NSProgressIndicator CreateView() + { + return new NSProgressIndicator + { + IsDisplayedWhenStopped = true, + Indeterminate = false, + Style = NSProgressIndicatorStyle.Bar, + MinValue = 0, + MaxValue = 1 + }; + } + + public static void MapPropertyProgress(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is NSProgressIndicator nSProgressIndicator)) + return; + + nSProgressIndicator.DoubleValue = progressBar.Progress; + } + + public static void MapPropertyProgressColor(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is NSProgressIndicator nSProgressIndicator)) + return; + + var progressColor = progressBar.ProgressColor; + + if (progressColor.IsDefault) + return; + + var newProgressColor = progressColor.ToNativeColor(); + + if (Equals(_currentColor, newProgressColor)) + { + if (nSProgressIndicator.ContentFilters?.FirstOrDefault() != _currentColorFilter) + { + nSProgressIndicator.ContentFilters = new CIFilter[] { _currentColorFilter }; + } + return; + } + + _currentColor = newProgressColor; + + _currentColorFilter = new CIColorPolynomial + { + RedCoefficients = new CIVector(_currentColor.RedComponent), + BlueCoefficients = new CIVector(_currentColor.BlueComponent), + GreenCoefficients = new CIVector(_currentColor.GreenComponent) + }; + + nSProgressIndicator.ContentFilters = new CIFilter[] { _currentColorFilter }; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Standard.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Standard.cs new file mode 100644 index 000000000000..bfc1c184daf7 --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Standard.cs @@ -0,0 +1,10 @@ +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyProgress(IViewRenderer renderer, IProgress progressBar) { } + public static void MapPropertyProgressColor(IViewRenderer renderer, IProgress progressBar) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Win32.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Win32.cs new file mode 100644 index 000000000000..b582d3fb26d7 --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.Win32.cs @@ -0,0 +1,22 @@ +using WProgressBar = System.Windows.Controls.ProgressBar; + +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer : AbstractViewRenderer + { + protected override WProgressBar CreateView() => new WProgressBar { Minimum = 0, Maximum = 1 }; + + public static void MapPropertyProgress(IViewRenderer renderer, IProgress progressBar) => (renderer as ProgressBarRenderer)?.UpdateProgress(); + public static void MapPropertyProgressColor(IViewRenderer renderer, IProgress progressBar) => (renderer as ProgressBarRenderer)?.UpdateProgressColor(); + + public virtual void UpdateProgressColor() + { + TypedNativeView.UpdateDependencyColor(WProgressBar.ForegroundProperty, VirtualView.ProgressColor.IsDefault ? Color.DeepSkyBlue : VirtualView.ProgressColor); + } + + public virtual void UpdateProgress() + { + TypedNativeView.Value = VirtualView.Progress; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.cs new file mode 100644 index 000000000000..7d37997d3c2e --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.cs @@ -0,0 +1,21 @@ +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer + { + public static PropertyMapper ProgressMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IProgress.Progress)] = MapPropertyProgress, + [nameof(IProgress.ProgressColor)] = MapPropertyProgressColor + }; + + public ProgressBarRenderer() : base(ProgressMapper) + { + + } + + public ProgressBarRenderer(PropertyMapper mapper) : base(mapper ?? ProgressMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.iOS.cs b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.iOS.cs new file mode 100644 index 000000000000..cbab686dee0c --- /dev/null +++ b/Maui.Core/Renderers/ProgressBar/ProgressBarRenderer.iOS.cs @@ -0,0 +1,28 @@ +using UIKit; + +namespace System.Maui.Platform +{ + public partial class ProgressBarRenderer : AbstractViewRenderer + { + protected override UIProgressView CreateView() + { + return new UIProgressView(UIProgressViewStyle.Default); + } + + public static void MapPropertyProgress(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is UIProgressView uIProgressView)) + return; + + uIProgressView.Progress = (float)progressBar.Progress; + } + + public static void MapPropertyProgressColor(IViewRenderer renderer, IProgress progressBar) + { + if (!(renderer.NativeView is UIProgressView uIProgressView)) + return; + + uIProgressView.ProgressTintColor = progressBar.ProgressColor == Color.Default ? null : progressBar.ProgressColor.ToNativeColor(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.Android.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.Android.cs new file mode 100644 index 000000000000..dea3982442fa --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.Android.cs @@ -0,0 +1,172 @@ +using AView = Android.Views.View; +using Android.Text; +using System.Linq; +using Android.Widget; +using Android.Views; +using System.Collections.Generic; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer : AbstractViewRenderer + { + EditText _editText; + TextColorSwitcher _textColorSwitcher; + TextColorSwitcher _hintColorSwitcher; + QueryListener _queryListener; + + protected override SearchView CreateView() + { + _queryListener = new QueryListener(VirtualView, this); + + var context = (Context as ContextThemeWrapper).BaseContext ?? Context; + var searchView = new SearchView(context); + + searchView.SetIconifiedByDefault(false); + searchView.Iconified = false; + _editText ??= searchView.GetChildrenOfType().FirstOrDefault(); + + if (_editText != null) + { + _textColorSwitcher = new TextColorSwitcher(_editText); + _hintColorSwitcher = new TextColorSwitcher(_editText); + } + + searchView.SetOnQueryTextListener(_queryListener); + + return searchView; + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdateTextColor(entry.Color); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdatePlaceholder(); + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdatePlaceholderColor(entry.PlaceholderColor); + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdateText(); + } + + public static void MapPropertyCancelColor(IViewRenderer renderer, ISearch search) + { + (renderer as SearchRenderer)?.UpdateCancelColor(); + } + + public static void MapPropertyMaxLength(IViewRenderer renderer, ITextInput view) + { + (renderer as SearchRenderer)?.UpdateMaxLength(); + } + + protected virtual void UpdateTextColor(Color color) + { + _textColorSwitcher?.UpdateTextColor(color); + } + + protected virtual void UpdatePlaceholderColor(Color color) + { + _hintColorSwitcher?.UpdateHintTextColor(color); + } + + protected virtual void UpdateMaxLength() + { + if (_editText == null) + return; + + var maxLength = VirtualView.MaxLength; + + //default + if (maxLength == -1) + return; + + var currentFilters = new List(_editText?.GetFilters() ?? new IInputFilter[0]); + + for (var i = 0; i < currentFilters.Count; i++) + { + if (currentFilters[i] is InputFilterLengthFilter) + { + currentFilters.RemoveAt(i); + break; + } + } + + currentFilters.Add(new InputFilterLengthFilter(maxLength)); + + _editText?.SetFilters(currentFilters.ToArray()); + + var currentControlText = TypedNativeView.Query; + + if (currentControlText.Length > maxLength) + TypedNativeView.SetQuery(currentControlText.Substring(0, maxLength), false); + } + + protected virtual void UpdatePlaceholder() + { + TypedNativeView.SetQueryHint(VirtualView.Placeholder); + } + + protected virtual void UpdateText() + { + string query = TypedNativeView.Query; + var text = VirtualView.Text; + if (query != text) + TypedNativeView.SetQuery(text, false); + } + + protected virtual void UpdateCancelColor() + { + int searchViewCloseButtonId = TypedNativeView.Resources.GetIdentifier("android:id/search_close_btn", null, null); + if (searchViewCloseButtonId != 0) + { + var image = TypedNativeView.FindViewById(searchViewCloseButtonId); + if (image != null && image.Drawable != null) + { + var cancelColor = VirtualView.CancelColor; + + if (!cancelColor.IsDefault) + image.Drawable.SetColorFilter(cancelColor, FilterMode.SrcIn); + else + image.Drawable.ClearColorFilter(); + } + } + } + + internal void ClearFocus() + { + TypedNativeView?.ClearFocus(); + } + } + + class QueryListener : Java.Lang.Object, SearchView.IOnQueryTextListener + { + ISearch _search; + SearchRenderer _searchRenderer; + + public QueryListener(ISearch search, SearchRenderer searchRenderer) + { + _searchRenderer = searchRenderer; + _search = search; + } + + bool SearchView.IOnQueryTextListener.OnQueryTextChange(string newText) + { + _search.Text = newText; + return true; + } + + bool SearchView.IOnQueryTextListener.OnQueryTextSubmit(string query) + { + _search.Search(); + _searchRenderer.ClearFocus(); + return true; + } + } +} diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.Mac.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.Mac.cs new file mode 100644 index 000000000000..bd509aef5fb5 --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.Mac.cs @@ -0,0 +1,97 @@ +using AppKit; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer : AbstractViewRenderer + { + + protected override NSSearchField CreateView() + { + var searchBar = new NSSearchField(); + //_defaultTextColor = textView.TextColor; + + //textView.EditingDidEnd += TextViewEditingDidEnd; + + return searchBar; + } + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdateTextColor(); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer is SearchRenderer searchRenderer)) + return; + + + searchRenderer.UpdatePlaceholder(); + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + if (!(renderer is SearchRenderer searchRenderer)) + return; + + searchRenderer.UpdatePlaceholder(); + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput view) + { + if (!(renderer.NativeView is NSSearchField searchField)) + return; + + if (!(renderer is SearchRenderer searchrenderer)) + return; + + searchField.StringValue = view.Text ?? ""; + searchrenderer.UpdateCancelButton(); + } + + public static void MapPropertyCancelColor(IViewRenderer renderer, ISearch view) + { + (renderer as SearchRenderer)?.UpdateCancelButton(); + } + + public static void MapPropertyMaxLength(IViewRenderer renderer, ITextInput view) + { + (renderer as SearchRenderer)?.UpdateMaxLength(); + } + public static void MapPropertyBackgroundColor(IViewRenderer renderer, IView view) + { + + } + + protected virtual void UpdateMaxLength() + { + + } + + protected virtual void UpdateTextColor() + { + var color = VirtualView.Color; + //TypedNativeView.TextColor = color.IsDefault ? _defaultTextColor : color.ToNativeColor(); + } + + protected virtual void UpdatePlaceholder() + { + var placeholder = VirtualView.Placeholder; + + if (placeholder == null) + { + return; + } + + var targetColor = VirtualView.PlaceholderColor; + //var color = targetColor.IsDefault ? ColorExtensions.SeventyPercentGrey : targetColor.ToNativeColor(); + + //var attributedPlaceholder = new NSAttributedString(str: placeholder, foregroundColor: color); + //TypedNativeView.AttributedPlaceholder = attributedPlaceholder; + } + + protected virtual void UpdateCancelButton() + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.Standard.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.Standard.cs new file mode 100644 index 000000000000..dbddd2d80c86 --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.Standard.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) { } + public static void MapPropertyCancelColor(IViewRenderer renderer, ISearch search) { } + public static void MapPropertyMaxLength(IViewRenderer renderer, ITextInput view) { } + public static void MapPropertyBackgroundColor(IViewRenderer renderer, IView view) { } + } +} diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.Win32.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.Win32.cs new file mode 100644 index 000000000000..1fffdc334b3a --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.Win32.cs @@ -0,0 +1,101 @@ +using System.Maui.Core.Controls; +using System.Windows.Input; +using System.Windows.Media; +using WControl = System.Windows.Controls.Control; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer : AbstractViewRenderer + { + const string DefaultPlaceholder = "Search"; + Brush _defaultPlaceholderColorBrush; + Brush _defaultTextColorBrush; + + protected override MauiTextBox CreateView() + { + var scope = new InputScope(); + var name = new InputScopeName(); + scope.Names.Add(name); + + var control = new MauiTextBox { InputScope = scope }; + control.KeyUp += OnTextBoxKeyUp; + control.TextChanged += OnTextTextBoxChanged; + + return control; + } + + protected override void SetupDefaults() + { + _defaultPlaceholderColorBrush = (Brush)WControl.ForegroundProperty.GetMetadata(typeof(MauiTextBox)).DefaultValue; + base.SetupDefaults(); + } + + protected override void DisposeView(MauiTextBox nativeView) + { + nativeView.KeyUp -= OnTextBoxKeyUp; + nativeView.TextChanged -= OnTextTextBoxChanged; + base.DisposeView(nativeView); + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) => (renderer as SearchRenderer)?.UpdateTextColor(); + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) => (renderer as SearchRenderer)?.UpdatePlaceholder(); + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) => (renderer as SearchRenderer)?.UpdatePlaceholderColor(); + public static void MapPropertyText(IViewRenderer renderer, ITextInput entry) => (renderer as SearchRenderer)?.UpdateText(); + public static void MapPropertyCancelColor(IViewRenderer renderer, ISearch search) { } + public static void MapPropertyMaxLength(IViewRenderer renderer, ITextInput view) { } + public static void MapPropertyBackgroundColor(IViewRenderer renderer, IView view) => ViewRenderer.MapBackgroundColor(renderer, view); + + public virtual void UpdatePlaceholder() + { + TypedNativeView.PlaceholderText = VirtualView.Placeholder ?? DefaultPlaceholder; + } + + public virtual void UpdatePlaceholderColor() + { + Color placeholderColor = VirtualView.PlaceholderColor; + + if (placeholderColor.IsDefault) + { + TypedNativeView.PlaceholderForegroundBrush = _defaultPlaceholderColorBrush; + return; + } + + TypedNativeView.PlaceholderForegroundBrush = placeholderColor.ToBrush(); + } + + public virtual void UpdateText() + { + TypedNativeView.Text = VirtualView.Text ?? ""; + } + + public virtual void UpdateTextColor() + { + Color textColor = VirtualView.Color; + + if (textColor.IsDefault) + { + if (_defaultTextColorBrush == null) + return; + + TypedNativeView.Foreground = _defaultTextColorBrush; + } + + if (_defaultTextColorBrush == null) + _defaultTextColorBrush = TypedNativeView.Foreground; + + TypedNativeView.Foreground = textColor.ToBrush(); + } + + void OnTextBoxKeyUp(object sender, KeyEventArgs keyEventArgs) + { + if (keyEventArgs.Key == Key.Enter) + VirtualView.Search(); + } + + void OnTextTextBoxChanged(object sender, System.Windows.Controls.TextChangedEventArgs textChangedEventArgs) + { + VirtualView.Text = TypedNativeView.Text; + } + + } +} diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.cs new file mode 100644 index 000000000000..edec9636466b --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer + { + public static PropertyMapper SearchMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IText.Color)] = MapPropertyColor, + [nameof(IText.Text)] = MapPropertyText, + + [nameof(ITextInput.Placeholder)] = MapPropertyPlaceholder, + [nameof(ITextInput.PlaceholderColor)] = MapPropertyPlaceholderColor, + [nameof(ITextInput.MaxLength)] = MapPropertyMaxLength, + + [nameof(ISearch.CancelColor)] = MapPropertyCancelColor, +#if __IOS__ + [nameof(IView.BackgroundColor)] = MapPropertyBackgroundColor, +#endif + }; + + public SearchRenderer() : base(SearchMapper) + { + + } + + public SearchRenderer(PropertyMapper mapper) : base(mapper ?? SearchMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/SearchBar/SearchRenderer.iOS.cs b/Maui.Core/Renderers/SearchBar/SearchRenderer.iOS.cs new file mode 100644 index 000000000000..6b66ac6b9521 --- /dev/null +++ b/Maui.Core/Renderers/SearchBar/SearchRenderer.iOS.cs @@ -0,0 +1,226 @@ +using System.Drawing; +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class SearchRenderer : AbstractViewRenderer + { + UIColor _cancelButtonTextColorDefaultDisabled; + UIColor _cancelButtonTextColorDefaultHighlighted; + UIColor _cancelButtonTextColorDefaultNormal; + + UIColor _defaultTextColor; + UIColor _defaultTintColor; + UITextField _textField; + bool _textWasTyped; + string _typedText; + + //UIToolbar _numericAccessoryView; + + protected override UISearchBar CreateView() + { + var searchBar = new UISearchBar(RectangleF.Empty) { ShowsCancelButton = true, BarStyle = UIBarStyle.Default }; + + var cancelButton = searchBar.FindDescendantView(); + _cancelButtonTextColorDefaultNormal = cancelButton.TitleColor(UIControlState.Normal); + _cancelButtonTextColorDefaultHighlighted = cancelButton.TitleColor(UIControlState.Highlighted); + _cancelButtonTextColorDefaultDisabled = cancelButton.TitleColor(UIControlState.Disabled); + + _textField ??= searchBar.FindDescendantView(); + + searchBar.CancelButtonClicked += OnCancelClicked; + searchBar.SearchButtonClicked += OnSearchButtonClicked; + searchBar.TextChanged += OnTextChanged; + searchBar.ShouldChangeTextInRange += ShouldChangeText; + + searchBar.OnEditingStarted += OnEditingStarted; + searchBar.OnEditingStopped += OnEditingEnded; + + return searchBar; + } + + public static void MapPropertyColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdateTextColor(); + } + + public static void MapPropertyPlaceholder(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdatePlaceholder(); + } + + public static void MapPropertyPlaceholderColor(IViewRenderer renderer, ITextInput entry) + { + (renderer as SearchRenderer)?.UpdatePlaceholder(); + } + + public static void MapPropertyText(IViewRenderer renderer, ITextInput view) + { + (renderer as SearchRenderer)?.UpdateText(); + } + + public static void MapPropertyCancelColor(IViewRenderer renderer, ISearch view) + { + (renderer as SearchRenderer)?.UpdateCancelButton(); + } + + public static void MapPropertyMaxLength(IViewRenderer renderer, ITextInput view) + { + (renderer as SearchRenderer)?.UpdateMaxLength(); + } + + public static void MapPropertyBackgroundColor(IViewRenderer renderer, IView view) + { + (renderer as SearchRenderer)?.UpdateBackgroundColor(view.BackgroundColor); + } + + protected virtual void UpdateTextColor() + { + if (_textField == null) + return; + + _defaultTextColor ??= _textField.TextColor; + + var targetColor = VirtualView.Color; + + _textField.TextColor = targetColor.IsDefault ? _defaultTextColor : targetColor.ToNativeColor(); + } + + protected virtual void UpdatePlaceholder() + { + if (_textField == null) + return; + + var placeholder = VirtualView.Placeholder; + + if (placeholder == null) + return; + + + var targetColor = VirtualView.PlaceholderColor; + + var color = VirtualView.IsEnabled && !targetColor.IsDefault ? targetColor : Color.LightGray; + + var attributedPlaceholder = new NSAttributedString(str: placeholder, foregroundColor: color.ToNativeColor()); + + _textField.AttributedPlaceholder = attributedPlaceholder; + } + + protected virtual void UpdateText() + { + // There is at least one scenario where modifying the Element's Text value from TextChanged + // can cause issues with a Korean keyboard. The characters normally combine into larger + // characters as they are typed, but if SetValueFromRenderer is used in that manner, + // it ignores the combination and outputs them individually. This hook only fires + // when typing, so by keeping track of whether or not text was typed, we can respect + // other changes to Element.Text. + if (!_textWasTyped) + TypedNativeView.Text = VirtualView.Text; + + UpdateCancelButton(); + } + + protected virtual void UpdateCancelButton() + { + TypedNativeView.ShowsCancelButton = !string.IsNullOrEmpty(TypedNativeView.Text); + + // We can't cache the cancel button reference because iOS drops it when it's not displayed + // and creates a brand new one when necessary, so we have to look for it each time + var cancelButton = TypedNativeView.FindDescendantView(); + + if (cancelButton == null) + return; + + var cancelButtonColor = VirtualView.CancelColor; + if (cancelButtonColor.IsDefault) + { + cancelButton.SetTitleColor(_cancelButtonTextColorDefaultNormal, UIControlState.Normal); + cancelButton.SetTitleColor(_cancelButtonTextColorDefaultHighlighted, UIControlState.Highlighted); + cancelButton.SetTitleColor(_cancelButtonTextColorDefaultDisabled, UIControlState.Disabled); + } + else + { + var nativeCancelButtonColor = cancelButtonColor.ToNativeColor(); + cancelButton.SetTitleColor(nativeCancelButtonColor, UIControlState.Normal); + cancelButton.SetTitleColor(nativeCancelButtonColor, UIControlState.Highlighted); + cancelButton.SetTitleColor(nativeCancelButtonColor, UIControlState.Disabled); + } + } + + protected virtual void UpdateMaxLength() + { + var currentControlText = TypedNativeView.Text; + var maxLength = VirtualView.MaxLength; + + //default + if (maxLength == -1) + return; + + if (currentControlText.Length > maxLength) + TypedNativeView.Text = currentControlText.Substring(0, maxLength); + } + + protected virtual void UpdateBackgroundColor(Color color) + { + + if (_defaultTintColor == null) + _defaultTintColor = TypedNativeView.BarTintColor; + + TypedNativeView.BarTintColor = color.IsDefault ? _defaultTintColor : color.ToNativeColor(); + + // updating BarTintColor resets the button color so we need to update the button color again + UpdateCancelButton(); + } + + void OnCancelClicked(object sender, EventArgs args) + { + VirtualView.Cancel(); + TypedNativeView?.ResignFirstResponder(); + } + + void OnEditingEnded(object sender, EventArgs e) + { + //focus off + } + + void OnEditingStarted(object sender, EventArgs e) + { + //focus on + } + + void OnSearchButtonClicked(object sender, EventArgs e) + { + VirtualView.Search(); + TypedNativeView?.ResignFirstResponder(); + } + + void OnTextChanged(object sender, UISearchBarTextChangedEventArgs a) + { + // This only fires when text has been typed into the SearchBar; see UpdateText() + // for why this is handled in this manner. + _textWasTyped = true; + _typedText = a.SearchText; + UpdateOnTextChanged(); + } + + void UpdateOnTextChanged() + { + VirtualView.Text = _typedText ?? string.Empty; + _textWasTyped = false; + UpdateCancelButton(); + } + + bool ShouldChangeText(UISearchBar searchBar, NSRange range, string text) + { + var maxLength = VirtualView?.MaxLength; + //default + if (maxLength == -1) + return true; + + var newLength = searchBar?.Text?.Length + text.Length - range.Length; + return newLength <= maxLength; + } + + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.Android.cs b/Maui.Core/Renderers/Slider/SliderRenderer.Android.cs new file mode 100644 index 000000000000..afb8261335e7 --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.Android.cs @@ -0,0 +1,136 @@ +using System.Maui.Core.Controls; +using Android.Content.Res; +using Android.Graphics; +using Android.OS; +using Android.Widget; + +namespace System.Maui.Platform +{ + public partial class SliderRenderer : AbstractViewRenderer + { + ColorStateList _defaultProgressTintList; + ColorStateList _defaultProgressBackgroundTintList; + PorterDuff.Mode _defaultProgressTintMode; + PorterDuff.Mode _defaultProgressBackgroundTintMode; + ColorFilter _defaultThumbColorFilter; + + protected override MauiSlider CreateView() + { + var slider = new MauiSlider(Context); + + slider.SetOnSeekBarChangeListener(new MauiSeekBarListener(VirtualView)); + + return slider; + } + + protected override void SetupDefaults() + { + base.SetupDefaults(); + + var mauiSlider = TypedNativeView; + + if (mauiSlider == null) + return; + + if (Build.VERSION.SdkInt > BuildVersionCodes.Kitkat) + { + _defaultThumbColorFilter = mauiSlider.Thumb.GetColorFilter(); + _defaultProgressTintMode = mauiSlider.ProgressTintMode; + _defaultProgressBackgroundTintMode = mauiSlider.ProgressBackgroundTintMode; + _defaultProgressTintList = mauiSlider.ProgressTintList; + _defaultProgressBackgroundTintList = mauiSlider.ProgressBackgroundTintList; + } + } + + public static void MapPropertyMinimum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.Min = (int)slider.Minimum; + } + + public static void MapPropertyMaximum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.Max = (int)slider.Maximum; + } + + public static void MapPropertyValue(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.Progress = (int)slider.Value; + } + + public static void MapPropertyMinimumTrackColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is MauiSlider mauiSlider)) + return; + + if (slider.MinimumTrackColor == Color.Default) + { + mauiSlider.ProgressTintList = sliderRenderer._defaultProgressTintList; + mauiSlider.ProgressTintMode = sliderRenderer._defaultProgressTintMode; + } + else + { + mauiSlider.ProgressTintList = ColorStateList.ValueOf(slider.MinimumTrackColor.ToNative()); + mauiSlider.ProgressTintMode = PorterDuff.Mode.SrcIn; + } + } + + public static void MapPropertyMaximumTrackColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is MauiSlider mauiSlider)) + return; + + if (slider.MaximumTrackColor == Color.Default) + { + mauiSlider.ProgressBackgroundTintList = sliderRenderer._defaultProgressBackgroundTintList; + mauiSlider.ProgressBackgroundTintMode = sliderRenderer._defaultProgressBackgroundTintMode; + } + else + { + mauiSlider.ProgressBackgroundTintList = ColorStateList.ValueOf(slider.MaximumTrackColor.ToNative()); + mauiSlider.ProgressBackgroundTintMode = PorterDuff.Mode.SrcIn; + } + } + + public static void MapPropertyThumbColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.Thumb.SetColorFilter(slider.ThumbColor, sliderRenderer._defaultThumbColorFilter, FilterMode.SrcIn); + } + + internal class MauiSeekBarListener : Java.Lang.Object, SeekBar.IOnSeekBarChangeListener + { + readonly ISlider _virtualView; + + public MauiSeekBarListener(ISlider virtualView) + { + _virtualView = virtualView; + } + + public void OnProgressChanged(SeekBar seekBar, int progress, bool fromUser) + { + _virtualView.Value = progress; + } + + public void OnStartTrackingTouch(SeekBar seekBar) + { + _virtualView.DragStarted(); + } + + public void OnStopTrackingTouch(SeekBar seekBar) + { + _virtualView.DragCompleted(); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.Mac.cs b/Maui.Core/Renderers/Slider/SliderRenderer.Mac.cs new file mode 100644 index 000000000000..fbb210a4aa39 --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.Mac.cs @@ -0,0 +1,79 @@ +using System.Maui.Core.Controls; +using AppKit; +using Foundation; + +namespace System.Maui.Platform +{ + public partial class SliderRenderer : AbstractViewRenderer + { + protected override MauiSlider CreateView() + { + var slider = new MauiSlider + { + Action = new ObjCRuntime.Selector(nameof(ValueChanged)) + }; + + return slider; + } + + protected override void DisposeView(MauiSlider slider) + { + slider.Action = null; + + base.DisposeView(slider); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.MinValue = (float)slider.Minimum; + } + + public static void MapPropertyMaximum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + mauiSlider.MaxValue = (float)slider.Maximum; + } + + public static void MapPropertyValue(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is MauiSlider mauiSlider)) + return; + + if (Math.Abs(slider.Value - mauiSlider.DoubleValue) > 0) + mauiSlider.DoubleValue = (float)slider.Value; + } + + public static void MapPropertyMinimumTrackColor(IViewRenderer renderer, ISlider slider) + { + + } + + public static void MapPropertyMaximumTrackColor(IViewRenderer renderer, ISlider slider) + { + + } + + public static void MapPropertyThumbColor(IViewRenderer renderer, ISlider slider) + { + + } + + [Export(nameof(ValueChanged))] + void ValueChanged() + { + VirtualView.Value = TypedNativeView.DoubleValue; + + var controlEvent = NSApplication.SharedApplication.CurrentEvent; + + if (controlEvent.Type == NSEventType.LeftMouseDown) + VirtualView.DragStarted(); + else if (controlEvent.Type == NSEventType.LeftMouseUp) + VirtualView.DragCompleted(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.Standard.cs b/Maui.Core/Renderers/Slider/SliderRenderer.Standard.cs new file mode 100644 index 000000000000..dffdc3649f40 --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.Standard.cs @@ -0,0 +1,14 @@ +namespace System.Maui.Platform +{ + public partial class SliderRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyMinimum(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyMaximum(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyValue(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyMinimumTrackColor(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyMaximumTrackColor(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyThumbColor(IViewRenderer renderer, ISlider slider) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.Win32.cs b/Maui.Core/Renderers/Slider/SliderRenderer.Win32.cs new file mode 100644 index 000000000000..3d3702342276 --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.Win32.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using WSlider = System.Windows.Controls.Slider; + +namespace System.Maui.Platform +{ + public partial class SliderRenderer : AbstractViewRenderer + { + protected override WSlider CreateView() + { + var slider = new WSlider(); + slider.ValueChanged += OnSliderValueChanged; + return slider; + } + + protected override void DisposeView(WSlider nativeView) + { + nativeView.ValueChanged -= OnSliderValueChanged; + base.DisposeView(nativeView); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, ISlider slider) => (renderer as SliderRenderer)?.UpdateMinimum(); + public static void MapPropertyMaximum(IViewRenderer renderer, ISlider slider) => (renderer as SliderRenderer)?.UpdateMaximum(); + public static void MapPropertyValue(IViewRenderer renderer, ISlider slider) => (renderer as SliderRenderer)?.UpdateValue(); + public static void MapPropertyMinimumTrackColor(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyMaximumTrackColor(IViewRenderer renderer, ISlider slider) { } + public static void MapPropertyThumbColor(IViewRenderer renderer, ISlider slider) { } + + public virtual void UpdateMinimum() + { + TypedNativeView.Minimum = VirtualView.Minimum; + } + + public virtual void UpdateMaximum() + { + TypedNativeView.Maximum = VirtualView.Maximum; + } + + public virtual void UpdateValue() + { + var newValue = VirtualView.Value; + if (TypedNativeView.Value != newValue) + TypedNativeView.Value = newValue; + } + + void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + if (TypedNativeView.Value != VirtualView.Value) + VirtualView.Value = TypedNativeView.Value; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.cs b/Maui.Core/Renderers/Slider/SliderRenderer.cs new file mode 100644 index 000000000000..5465cc5b041c --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.cs @@ -0,0 +1,25 @@ +namespace System.Maui.Platform +{ + public partial class SliderRenderer + { + public static PropertyMapper SliderMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(ISlider.Minimum)] = MapPropertyMinimum, + [nameof(ISlider.Maximum)] = MapPropertyMaximum, + [nameof(ISlider.Value)] = MapPropertyValue, + [nameof(ISlider.MinimumTrackColor)] = MapPropertyMinimumTrackColor, + [nameof(ISlider.MaximumTrackColor)] = MapPropertyMaximumTrackColor, + [nameof(ISlider.ThumbColor)] = MapPropertyThumbColor + }; + + public SliderRenderer() : base(SliderMapper) + { + + } + + public SliderRenderer(PropertyMapper mapper) : base(mapper ?? SliderMapper) + { + + } + } +} diff --git a/Maui.Core/Renderers/Slider/SliderRenderer.iOS.cs b/Maui.Core/Renderers/Slider/SliderRenderer.iOS.cs new file mode 100644 index 000000000000..342fde94c136 --- /dev/null +++ b/Maui.Core/Renderers/Slider/SliderRenderer.iOS.cs @@ -0,0 +1,114 @@ +using UIKit; + +namespace System.Maui.Platform +{ + public partial class SliderRenderer : AbstractViewRenderer + { + UIColor _defaultMinTrackColor; + UIColor _defaultMaxTrackColor; + UIColor _defaultThumbColor; + + protected override UISlider CreateView() + { + var slider = new UISlider(); + + UpdateDefaultColors(slider); + + slider.ValueChanged += OnControlValueChanged; + + slider.AddTarget(OnTouchDownControlEvent, UIControlEvent.TouchDown); + slider.AddTarget(OnTouchUpControlEvent, UIControlEvent.TouchUpInside | UIControlEvent.TouchUpOutside); + + return slider; + } + + protected override void DisposeView(UISlider slider) + { + slider.ValueChanged -= OnControlValueChanged; + slider.RemoveTarget(OnTouchDownControlEvent, UIControlEvent.TouchDown); + slider.RemoveTarget(OnTouchUpControlEvent, UIControlEvent.TouchUpInside | UIControlEvent.TouchUpOutside); + + base.DisposeView(slider); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is UISlider uISlider)) + return; + + uISlider.MinValue = (float)slider.Minimum; + } + + public static void MapPropertyMaximum(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is UISlider uISlider)) + return; + + uISlider.MaxValue = (float)slider.Maximum; + } + + public static void MapPropertyValue(IViewRenderer renderer, ISlider slider) + { + if (!(renderer.NativeView is UISlider uISlider)) + return; + + if ((float)slider.Value != uISlider.Value) + uISlider.Value = (float)slider.Value; + } + + public static void MapPropertyMinimumTrackColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is UISlider uISlider)) + return; + + if (slider.MinimumTrackColor == Color.Default) + uISlider.MinimumTrackTintColor = sliderRenderer._defaultMinTrackColor; + else + uISlider.MinimumTrackTintColor = slider.MinimumTrackColor.ToNativeColor(); + } + + public static void MapPropertyMaximumTrackColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is UISlider uISlider)) + return; + + if (slider.MaximumTrackColor == Color.Default) + uISlider.MaximumTrackTintColor = sliderRenderer._defaultMaxTrackColor; + else + uISlider.MaximumTrackTintColor = slider.MaximumTrackColor.ToNativeColor(); + } + + public static void MapPropertyThumbColor(IViewRenderer renderer, ISlider slider) + { + if (!(renderer is SliderRenderer sliderRenderer) || !(renderer.NativeView is UISlider uISlider)) + return; + + if (slider.ThumbColor == Color.Default) + uISlider.ThumbTintColor = sliderRenderer._defaultThumbColor; + else + uISlider.ThumbTintColor = slider.ThumbColor.ToNativeColor(); + } + + void UpdateDefaultColors(UISlider uISlider) + { + _defaultMinTrackColor = uISlider.MinimumTrackTintColor; + _defaultMaxTrackColor = uISlider.MaximumTrackTintColor; + _defaultThumbColor = uISlider.ThumbTintColor; + } + + void OnControlValueChanged(object sender, EventArgs eventArgs) + { + VirtualView.Value = TypedNativeView.Value; + } + + void OnTouchDownControlEvent(object sender, EventArgs e) + { + VirtualView.DragStarted(); + } + + void OnTouchUpControlEvent(object sender, EventArgs e) + { + VirtualView.DragCompleted(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.Android.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.Android.cs new file mode 100644 index 000000000000..1bc340cbfa90 --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.Android.cs @@ -0,0 +1,57 @@ +using Android.Widget; +using Android.Views; +using AButton = Android.Widget.Button; + +namespace System.Maui.Platform +{ + public partial class StepperRenderer : AbstractViewRenderer , IStepperRenderer + { + AButton _downButton; + AButton _upButton; + + protected override LinearLayout CreateView() + { + var aStepper = new LinearLayout(Context) + { + Orientation = Android.Widget.Orientation.Horizontal, + Focusable = true, + DescendantFocusability = DescendantFocusability.AfterDescendants + }; + + StepperRendererManager.CreateStepperButtons(this, out _downButton, out _upButton); + aStepper.AddView(_downButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.MatchParent)); + aStepper.AddView(_upButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.MatchParent)); + + return aStepper; + } + + public static void MapPropertyMinimum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyMaximum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyIncrement(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyValue(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + + public static void MapPropertyIsEnabled(IViewRenderer renderer, IStepper slider) + { + ViewRenderer.MapPropertyIsEnabled(renderer, slider); + (renderer as StepperRenderer)?.UpdateButtons(); + } + + public virtual void UpdateButtons() + { + StepperRendererManager.UpdateButtons(this, _downButton, _upButton); + } + + IStepper IStepperRenderer.Element => VirtualView; + + AButton IStepperRenderer.UpButton => _upButton; + + AButton IStepperRenderer.DownButton => _downButton; + + AButton IStepperRenderer.CreateButton() + { + var button = new AButton(Context); + + return button; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.Mac.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.Mac.cs new file mode 100644 index 000000000000..b2df58422b06 --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.Mac.cs @@ -0,0 +1,49 @@ +using AppKit; + +namespace System.Maui.Platform +{ + public partial class StepperRenderer : AbstractViewRenderer + { + protected override NSStepper CreateView() + { + var nSStepper = new NSStepper(); + nSStepper.Activated += OnStepperActivated; + return nSStepper; + } + + protected override void DisposeView(NSStepper nSStepper) + { + nSStepper.Activated -= OnStepperActivated; + base.DisposeView(nSStepper); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateMinimum(); + public static void MapPropertyMaximum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateMaximum(); + public static void MapPropertyIncrement(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateIncrement(); + public static void MapPropertyValue(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateValue(); + + public virtual void UpdateIncrement() + { + TypedNativeView.Increment = VirtualView.Increment; + } + + public virtual void UpdateMaximum() + { + TypedNativeView.MaxValue = VirtualView.Maximum; + } + + public virtual void UpdateMinimum() + { + TypedNativeView.MinValue = VirtualView.Minimum; + } + + public virtual void UpdateValue() + { + if (Math.Abs(TypedNativeView.DoubleValue - VirtualView.Value) > 0) + TypedNativeView.DoubleValue = VirtualView.Value; + } + + void OnStepperActivated(object sender, EventArgs e) => + VirtualView.Value = TypedNativeView.DoubleValue; + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.Standard.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.Standard.cs new file mode 100644 index 000000000000..441a49ff3c66 --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.Standard.cs @@ -0,0 +1,12 @@ +namespace System.Maui.Platform +{ + public partial class StepperRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertyMinimum(IViewRenderer renderer, IStepper slider) { } + public static void MapPropertyMaximum(IViewRenderer renderer, IStepper slider) { } + public static void MapPropertyIncrement(IViewRenderer renderer, IStepper slider) { } + public static void MapPropertyValue(IViewRenderer renderer, IStepper slider) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.Win32.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.Win32.cs new file mode 100644 index 000000000000..1bc07c3a41a2 --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.Win32.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using WButton = System.Windows.Controls.Button; + +namespace System.Maui.Platform +{ + public partial class StepperRenderer : AbstractViewRenderer + { + readonly StackPanel _panel = new StackPanel(); + WButton _downButton; + WButton _upButton; + + protected override Border CreateView() + { + var control = CreateControl(); + + _upButton.Click += UpButtonOnClick; + _downButton.Click += DownButtonOnClick; + + return control; + } + + protected override void DisposeView(Border border) + { + _upButton.Click -= UpButtonOnClick; + _downButton.Click -= DownButtonOnClick; + + base.DisposeView(border); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyMaximum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyIncrement(IViewRenderer renderer, IStepper slider) { } + public static void MapPropertyValue(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateButtons(); + public static void MapPropertyIsEnabled(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateEnabled(); + + public virtual void UpdateEnabled() + { + _panel.IsEnabled = VirtualView.IsEnabled; + } + + public virtual void UpdateButtons() + { + var value = VirtualView.Value; + _upButton.IsEnabled = value < VirtualView.Maximum; + _downButton.IsEnabled = value > VirtualView.Minimum; + } + + Border CreateControl() + { + var border = new Border() { Child = _panel }; + _panel.HorizontalAlignment = HorizontalAlignment.Right; + _panel.Orientation = System.Windows.Controls.Orientation.Horizontal; + + _upButton = new WButton { Content = "+", Width = 100 }; + _downButton = new WButton { Content = "-", Width = 100 }; + + _panel.Children.Add(_downButton); + _panel.Children.Add(_upButton); + return border; + } + + void DownButtonOnClick(object sender, RoutedEventArgs routedEventArgs) + { + VirtualView.Value = Math.Max(VirtualView.Minimum, VirtualView.Value - VirtualView.Increment); + } + + void UpButtonOnClick(object sender, RoutedEventArgs routedEventArgs) + { + VirtualView.Value = Math.Min(VirtualView.Maximum, VirtualView.Value + VirtualView.Increment); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.cs new file mode 100644 index 000000000000..0ae414f859e0 --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.cs @@ -0,0 +1,26 @@ +namespace System.Maui.Platform +{ + public partial class StepperRenderer + { + public static PropertyMapper StepperMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IStepper.Minimum)] = MapPropertyMinimum, + [nameof(IStepper.Maximum)] = MapPropertyMaximum, + [nameof(IStepper.Increment)] = MapPropertyIncrement, + [nameof(IStepper.Value)] = MapPropertyValue +#if __ANDROID__ || NETCOREAPP + ,[nameof(IStepper.IsEnabled)] = MapPropertyIsEnabled +#endif + }; + + public StepperRenderer() : base(StepperMapper) + { + + } + + public StepperRenderer(PropertyMapper mapper) : base(mapper ?? StepperMapper) + { + + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Stepper/StepperRenderer.iOS.cs b/Maui.Core/Renderers/Stepper/StepperRenderer.iOS.cs new file mode 100644 index 000000000000..0f4e0bf892aa --- /dev/null +++ b/Maui.Core/Renderers/Stepper/StepperRenderer.iOS.cs @@ -0,0 +1,50 @@ +using System.Drawing; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class StepperRenderer : AbstractViewRenderer + { + protected override UIStepper CreateView() + { + var uIStepper = new UIStepper(RectangleF.Empty); + uIStepper.ValueChanged += OnValueChanged; + return uIStepper; + } + + protected override void DisposeView(UIStepper uIStepper) + { + uIStepper.ValueChanged -= OnValueChanged; + base.DisposeView(uIStepper); + } + + public static void MapPropertyMinimum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateMinimum(); + public static void MapPropertyMaximum(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateMaximum(); + public static void MapPropertyIncrement(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateIncrement(); + public static void MapPropertyValue(IViewRenderer renderer, IStepper slider) => (renderer as StepperRenderer)?.UpdateValue(); + + void OnValueChanged(object sender, EventArgs e) + => VirtualView.Value = TypedNativeView.Value; + + public virtual void UpdateIncrement() + { + TypedNativeView.StepValue = VirtualView.Increment; + } + + public virtual void UpdateMaximum() + { + TypedNativeView.MaximumValue = VirtualView.Maximum; + } + + public virtual void UpdateMinimum() + { + TypedNativeView.MinimumValue = VirtualView.Minimum; + } + + public virtual void UpdateValue() + { + if (TypedNativeView.Value != VirtualView.Value) + TypedNativeView.Value = VirtualView.Value; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.Android.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.Android.cs new file mode 100644 index 000000000000..4eb1d6a1fae7 --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.Android.cs @@ -0,0 +1,101 @@ +using Android.Graphics.Drawables; +using Android.Widget; +using ASwitch = Android.Widget.Switch; + +namespace System.Maui.Platform +{ + public partial class SwitchRenderer : AbstractViewRenderer + { + Drawable _defaultTrackDrawable; + bool _changedThumbColor; + OnListener _onListener; + + protected override ASwitch CreateView() + { + _onListener = new OnListener(this); + var aswitch = new ASwitch(Context); + aswitch.SetOnCheckedChangeListener(_onListener); + return aswitch; + } + + protected override void SetupDefaults() + { + _defaultTrackDrawable = TypedNativeView.TrackDrawable; + base.SetupDefaults(); + } + + protected override void DisposeView(ASwitch nativeView) + { + if(_onListener != null) + { + nativeView.SetOnCheckedChangeListener(null); + _onListener = null; + } + base.DisposeView(nativeView); + } + + public virtual void UpdateIsOn() + { + TypedNativeView.Checked = VirtualView.IsOn; + } + + public virtual void UpdateOnColor() + { + if (TypedNativeView.Checked) + { + var onColor = VirtualView.OnColor; + + if (onColor.IsDefault) + { + TypedNativeView.TrackDrawable = _defaultTrackDrawable; + } + else + { + TypedNativeView.TrackDrawable?.SetColorFilter(onColor.ToNative(), FilterMode.Multiply); + + } + } + else + { + TypedNativeView.TrackDrawable?.ClearColorFilter(); + } + + } + + public virtual void UpdateThumbColor() + { + var thumbColor = VirtualView.ThumbColor; + if (!thumbColor.IsDefault) + { + TypedNativeView.ThumbDrawable.SetColorFilter(thumbColor, FilterMode.Multiply); + _changedThumbColor = true; + } + else + { + if (_changedThumbColor) + { + TypedNativeView.ThumbDrawable?.ClearColorFilter(); + _changedThumbColor = false; + } + } + TypedNativeView.ThumbDrawable.SetColorFilter(thumbColor, FilterMode.Multiply); + } + + public virtual void SetIsOn(bool isChecked) => VirtualView.IsOn = isChecked; + } + + class OnListener : Java.Lang.Object, CompoundButton.IOnCheckedChangeListener + { + SwitchRenderer _switchRenderer; + + public OnListener(SwitchRenderer switchRenderer) + { + _switchRenderer = switchRenderer; + } + + void CompoundButton.IOnCheckedChangeListener.OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + _switchRenderer.UpdateOnColor(); + } + } +} diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.Mac.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.Mac.cs new file mode 100644 index 000000000000..6ec7e52ed79c --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.Mac.cs @@ -0,0 +1,44 @@ +using System; +using AppKit; + +namespace System.Maui.Platform +{ + public partial class SwitchRenderer : AbstractViewRenderer + { + protected override NSButton CreateView() + { + var nativeView = new NSButton + { + AllowsMixedState = false, + Title = string.Empty + }; + nativeView.Activated += NsSwitchActivated; + return nativeView; + } + + protected override void DisposeView(NSButton nativeView) + { + nativeView.Activated -= NsSwitchActivated; + base.DisposeView(nativeView); + } + + public virtual void UpdateIsOn() + { + TypedNativeView.State = VirtualView.IsOn ? NSCellStateValue.On : NSCellStateValue.Off; + } + + public virtual void SetIsOn() + => VirtualView.IsOn = TypedNativeView.State == NSCellStateValue.On; + + public virtual void UpdateOnColor() { } + + public virtual void UpdateThumbColor() { } + + void NsSwitchActivated(object sender, EventArgs e) + { + SetIsOn(); + } + + + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.Standard.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.Standard.cs new file mode 100644 index 000000000000..7fc3bf548363 --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.Standard.cs @@ -0,0 +1,14 @@ +using System; +namespace System.Maui.Platform +{ + public partial class SwitchRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public virtual void UpdateIsOn() { } + + public virtual void UpdateOnColor() { } + + public virtual void UpdateThumbColor() { } + } +} diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.Win32.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.Win32.cs new file mode 100644 index 000000000000..41a1b42aeb32 --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.Win32.cs @@ -0,0 +1,27 @@ +using System; +using WCheckBox = System.Windows.Controls.CheckBox; + +namespace System.Maui.Platform +{ + public partial class SwitchRenderer : AbstractViewRenderer + { + protected override WCheckBox CreateView() + { + var checkBox = new WCheckBox(); + checkBox.Checked += OnChecked; + checkBox.Unchecked += OnChecked; + return checkBox; + } + + public virtual void UpdateIsOn() => TypedNativeView.IsChecked = VirtualView.IsOn; + + public virtual void UpdateOnColor() { } + + public virtual void UpdateThumbColor() { } + + void OnChecked(object sender, System.Windows.RoutedEventArgs e) + { + VirtualView.IsOn = TypedNativeView.IsChecked.HasValue ? TypedNativeView.IsChecked.Value : false; + } + } +} diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.cs new file mode 100644 index 000000000000..63d72b680df4 --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.cs @@ -0,0 +1,32 @@ +using System; +namespace System.Maui.Platform +{ + public partial class SwitchRenderer + { + public static PropertyMapper SwitchMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(ISwitch.IsOn)] = MapPropertyIsOn, + [nameof(ISwitch.OnColor)] = MapPropertyOnColor, + [nameof(ISwitch.ThumbColor)] = MapPropertyThumbColor + }; + + public SwitchRenderer() : base(SwitchMapper) { } + + public SwitchRenderer(PropertyMapper mapper) : base(mapper ?? SwitchMapper) { } + + public static void MapPropertyIsOn(IViewRenderer renderer, ISwitch @switch) + { + (renderer as SwitchRenderer)?.UpdateIsOn(); + } + + public static void MapPropertyOnColor(IViewRenderer renderer, ISwitch @switch) + { + (renderer as SwitchRenderer)?.UpdateOnColor(); + } + + public static void MapPropertyThumbColor(IViewRenderer renderer, ISwitch @switch) + { + (renderer as SwitchRenderer)?.UpdateThumbColor(); + } + } +} diff --git a/Maui.Core/Renderers/Switch/SwitchRenderer.iOS.cs b/Maui.Core/Renderers/Switch/SwitchRenderer.iOS.cs new file mode 100644 index 000000000000..4ebc90764826 --- /dev/null +++ b/Maui.Core/Renderers/Switch/SwitchRenderer.iOS.cs @@ -0,0 +1,58 @@ +using System; +using System.Drawing; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class SwitchRenderer : AbstractViewRenderer + { + UIColor _defaultOnColor; + UIColor _defaultThumbColor; + + protected override UISwitch CreateView() + { + var nativeView = new UISwitch(RectangleF.Empty); + nativeView.ValueChanged += UISwitchValueChanged; + return nativeView; + } + + protected override void SetupDefaults() + { + _defaultOnColor = UISwitch.Appearance.OnTintColor; + _defaultThumbColor = UISwitch.Appearance.ThumbTintColor; + base.SetupDefaults(); + } + + protected override void DisposeView(UISwitch nativeView) + { + nativeView.ValueChanged -= UISwitchValueChanged; + base.DisposeView(nativeView); + } + + public virtual void UpdateIsOn() + { + TypedNativeView.SetState(VirtualView.IsOn, true); + } + + public virtual void SetIsOn() => + VirtualView.IsOn = TypedNativeView.On; + + public virtual void UpdateOnColor() + { + var onColor = VirtualView.OnColor; + + TypedNativeView.OnTintColor = onColor.IsDefault ? _defaultOnColor : onColor.ToNativeColor(); + } + + public virtual void UpdateThumbColor() + { + var thumbColor = VirtualView.ThumbColor; + TypedNativeView.ThumbTintColor = thumbColor.IsDefault ? _defaultThumbColor : thumbColor.ToNativeColor(); + } + + void UISwitchValueChanged(object sender, EventArgs e) + { + SetIsOn(); + } + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Android.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Android.cs new file mode 100644 index 000000000000..3a2adff6bc9e --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Android.cs @@ -0,0 +1,84 @@ +using Android.App; +using AndroidX.AppCompat.Widget; + +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer : AbstractViewRenderer + { + TextColorSwitcher _textColorSwitcher; + TimePickerDialog _dialog; + + protected override AppCompatTextView CreateView() + { + var text = new PickerView(Context) + { + HidePicker = HidePickerDialog, + ShowPicker = ShowPickerDialog + }; + + _textColorSwitcher = new TextColorSwitcher(text); + return text; + } + + protected override void DisposeView(AppCompatTextView nativeView) + { + _textColorSwitcher = null; + if (_dialog != null) + { + _dialog.Hide(); + _dialog = null; + } + + base.DisposeView(nativeView); + } + + protected virtual TimePickerDialog CreateTimePickerDialog(int hour, int minute) + { + void onTimeSetCallback(object obj, TimePickerDialog.TimeSetEventArgs args) + { + VirtualView.SelectedTime = new TimeSpan(args.HourOfDay, args.Minute, 0); + TypedNativeView.Text = VirtualView.Text; + } + + var dialog = new TimePickerDialog(Context, onTimeSetCallback, hour, minute, Use24HourClock()); + + return dialog; + } + + private bool Use24HourClock() + { + var clock = VirtualView.ClockIdentifier; + if (clock == string.Empty) + { + // No clock specified, so let's do our best to figure out the default for the locale + if (Threading.Thread.CurrentThread.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains("H")) + { + clock = ClockIdentifiers.TwentyFourHour; + } + } + + return clock == ClockIdentifiers.TwentyFourHour; + } + + private void ShowPickerDialog() + { + var time = VirtualView.SelectedTime; + ShowPickerDialog(time.Hours, time.Minutes); + } + + // This overload is here so we can pass in the current values from the dialog + // on an orientation change (so that orientation changes don't cause the user's date selection progress + // to be lost). Not useful until we have orientation changed events. + private void ShowPickerDialog(int hour, int minute) + { + _dialog = CreateTimePickerDialog(hour, minute); + _dialog.Show(); + } + + private void HidePickerDialog() + { + _dialog?.Hide(); + } + + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Mac.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Mac.cs new file mode 100644 index 000000000000..bcaf42b169e9 --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Mac.cs @@ -0,0 +1,48 @@ +using AppKit; +using Foundation; + +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer : AbstractViewRenderer + { + protected override NSDatePicker CreateView() + { + var nativeView = new MauiNSDatePicker + { + DatePickerMode = NSDatePickerMode.Single, + TimeZone = new NSTimeZone("UTC"), + DatePickerStyle = NSDatePickerStyle.TextFieldAndStepper, + DatePickerElements = NSDatePickerElementFlags.HourMinuteSecond + }; + + nativeView.ValidateProposedDateValue += HandleValueChanged; + + return nativeView; + } + + protected override void DisposeView(NSDatePicker nativeView) + { + nativeView.ValidateProposedDateValue -= HandleValueChanged; + base.DisposeView(nativeView); + } + + public static void MapPropertySelectedTime(IViewRenderer renderer, ITimePicker timePicker) + { + (renderer as TimePickerRenderer)?.UpdateSelectedTime(); + } + + public virtual void UpdateSelectedTime() + { + var time = new DateTime(2001, 1, 1).Add(VirtualView.SelectedTime); + var newDate = time.ToNSDate(); + if (!Equals(TypedNativeView.DateValue, newDate)) + TypedNativeView.DateValue = newDate; + } + + void HandleValueChanged(object sender, NSDatePickerValidatorEventArgs e) + { + VirtualView.SelectedTime = e.ProposedDateValue.ToDateTime().Date - new DateTime(2001, 1, 1); + } + + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Standard.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Standard.cs new file mode 100644 index 000000000000..65b80edb8b50 --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Standard.cs @@ -0,0 +1,7 @@ +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Win32.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Win32.cs new file mode 100644 index 000000000000..5122432641c1 --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.Win32.cs @@ -0,0 +1,64 @@ +using System.Maui.Core.Controls; +using System.Windows; +using System.Windows.Media; + +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer : AbstractViewRenderer + { + Brush _defaultBrush; + FontFamily _defaultFontFamily; + + protected override MauiTimePicker CreateView() + { + var picker = new MauiTimePicker(); + picker.TimeChanged += OnControlTimeChanged; + picker.Loaded += OnControlLoaded; + return picker; + } + + protected override void SetupDefaults() + { + base.SetupDefaults(); + } + + protected override void DisposeView(MauiTimePicker nativeView) + { + nativeView.TimeChanged -= OnControlTimeChanged; + nativeView.Loaded -= OnControlLoaded; + base.DisposeView(nativeView); + } + + public static void MapPropertySelectedTime(IViewRenderer renderer, ITimePicker timePicker) => (renderer as TimePickerRenderer)?.UpdateTime(); + public static void MapPropertyColor(IViewRenderer renderer, ITimePicker timePicker) => (renderer as TimePickerRenderer)?.UpdateTextColor(); + + public virtual void UpdateTime() + { + TypedNativeView.Time = VirtualView.SelectedTime; + } + + public virtual void UpdateTextColor() + { + Color color = VirtualView.Color; + TypedNativeView.Foreground = color.IsDefault ? (_defaultBrush ?? color.ToBrush()) : color.ToBrush(); + } + + void OnControlLoaded(object sender, RoutedEventArgs routedEventArgs) + { + // The defaults from the control template won't be available + // right away; we have to wait until after the template has been applied + _defaultBrush = TypedNativeView.Foreground; + _defaultFontFamily = TypedNativeView.FontFamily; + } + + void OnControlTimeChanged(object sender, TimeChangedEventArgs e) + { + VirtualView.SelectedTime = e.NewTime.HasValue ? e.NewTime.Value : default(TimeSpan); + } + + void UpdateTimeFormat() + { + + } + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.cs new file mode 100644 index 000000000000..1764f6af11b5 --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.cs @@ -0,0 +1,28 @@ +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer + { + public static PropertyMapper TimePickerMapper = new PropertyMapper(LabelRenderer.ITextMapper) + { +#if NETCOREAPP + [nameof(ITimePicker.SelectedTime)] = MapPropertySelectedTime, + [nameof(ITimePicker.Color)] = MapPropertyColor, +#elif __MACOS__ + [nameof(ITimePicker.SelectedTime)] = MapPropertySelectedTime, +#else + [nameof(ITimePicker.SelectedTime)] = LabelRenderer.MapPropertyText, +#endif + + }; + + public TimePickerRenderer() : base(TimePickerMapper) + { + + } + + public TimePickerRenderer(PropertyMapper mapper) : base(mapper) + { + } + + } +} diff --git a/Maui.Core/Renderers/TimePicker/TimePickerRenderer.iOS.cs b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.iOS.cs new file mode 100644 index 000000000000..8822b6149b58 --- /dev/null +++ b/Maui.Core/Renderers/TimePicker/TimePickerRenderer.iOS.cs @@ -0,0 +1,85 @@ +using System.Drawing; +using Foundation; +using UIKit; + +namespace System.Maui.Platform +{ + public partial class TimePickerRenderer : AbstractViewRenderer + { + UIColor _defaultTextColor; + UIDatePicker _timePicker; + + protected override PickerView CreateView() + { + var pickerView = new PickerView(); + _defaultTextColor = pickerView.TextColor; + + _timePicker = new UIDatePicker + { + Mode = UIDatePickerMode.Time, + TimeZone = new NSTimeZone("UTC"), + Date = new DateTime(VirtualView.SelectedTime.Ticks).ToNSDate(), + Locale = GetLocaleForClock() + }; + + var width = (float)UIScreen.MainScreen.Bounds.Width; + var toolbar = new UIToolbar(new RectangleF(0, 0, width, 44)) { BarStyle = UIBarStyle.Default, Translucent = true }; + var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace); + var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => + { + VirtualView.SelectedTime = _timePicker.Date.ToDateTime().TimeOfDay; + pickerView.ResignFirstResponder(); + }); + + toolbar.SetItems(new[] { spacer, doneButton }, false); + + _timePicker.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + toolbar.AutoresizingMask = UIViewAutoresizing.FlexibleHeight; + + pickerView.SetInputView(_timePicker); + pickerView.SetInputAccessoryView(toolbar); + + return pickerView; + } + + protected override void DisposeView(PickerView nativeView) + { + _defaultTextColor = null; + _timePicker = null; + nativeView.SetInputView(null); + nativeView.SetInputAccessoryView(null); + base.DisposeView(nativeView); + } + + private NSLocale GetLocaleForClock() + { + const string enUs = "en_US"; + const string enGb = "en_GB"; + + var clock = VirtualView.ClockIdentifier; + var currentLocale = NSLocale.CurrentLocale; + + if (currentLocale.Identifier == enUs && clock == ClockIdentifiers.TwelveHour) + { + return currentLocale; + } + + if (currentLocale.Identifier != enUs && clock == ClockIdentifiers.TwentyFourHour) + { + return currentLocale; + } + + if (clock == ClockIdentifiers.TwentyFourHour) + { + return NSLocale.FromLocaleIdentifier(enGb); + } + + if (clock == ClockIdentifiers.TwelveHour) + { + return NSLocale.FromLocaleIdentifier(enUs); + } + + return currentLocale; + } + } +} diff --git a/Maui.Core/Renderers/View/AbstractViewRenderer.Android.cs b/Maui.Core/Renderers/View/AbstractViewRenderer.Android.cs new file mode 100644 index 000000000000..182204216908 --- /dev/null +++ b/Maui.Core/Renderers/View/AbstractViewRenderer.Android.cs @@ -0,0 +1,74 @@ +using System.ComponentModel; +using System.Runtime.Versioning; +using Android.Content; +using Android.Views; + +namespace System.Maui.Platform +{ + public partial class AbstractViewRenderer : IAndroidViewRenderer + { + public void SetContext (Context context) => Context = context; + public Context Context { get; private set; } + + public void SetFrame(Rectangle frame) + { + var nativeView = View; + if (nativeView == null) + return; + + if (frame.Width < 0 || frame.Height < 0) + { + // This is just some initial Forms value nonsense, nothing is actually laying out yet + return; + } + + var left = Context.ToPixels(frame.Left); + var top = Context.ToPixels(frame.Top); + var bottom = Context.ToPixels(frame.Bottom); + var right = Context.ToPixels(frame.Right); + var width = Context.ToPixels(frame.Width); + var height = Context.ToPixels(frame.Height); + + if (nativeView.LayoutParameters == null) + { + nativeView.LayoutParameters = new ViewGroup.LayoutParams((int)width, (int)height); + } + else + { + nativeView.LayoutParameters.Width = (int)width; + nativeView.LayoutParameters.Height = (int)height; + } + + nativeView.Layout((int)left, (int)top, (int)right, (int)bottom); + } + + public virtual SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + if (TypedNativeView == null) + { + return new SizeRequest(Size.Zero); + } + + var deviceWidthConstraint = Context.ToPixels(widthConstraint); + var deviceHeightConstraint = Context.ToPixels(heightConstraint); + + var widthSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceWidthConstraint); + var heightSpec = MeasureSpecMode.AtMost.MakeMeasureSpec((int)deviceHeightConstraint); + + TypedNativeView.Measure(widthSpec, heightSpec); + + var deviceIndependentSize = Context.FromPixels(TypedNativeView.MeasuredWidth, TypedNativeView.MeasuredHeight); + + return new SizeRequest(deviceIndependentSize); + } + + void SetupContainer () { + ContainerView = new Core.Controls.ContainerView (this.Context) { + MainView = this.TypedNativeView, + }; + } + + void RemoveContainer () { + } + } +} diff --git a/Maui.Core/Renderers/View/AbstractViewRenderer.MaciOS.cs b/Maui.Core/Renderers/View/AbstractViewRenderer.MaciOS.cs new file mode 100644 index 000000000000..63db70d8e203 --- /dev/null +++ b/Maui.Core/Renderers/View/AbstractViewRenderer.MaciOS.cs @@ -0,0 +1,36 @@ +using System.Maui.Core.Controls; + +#if __MOBILE__ +using NativeColor = UIKit.UIColor; +#else +using NativeColor = AppKit.NSColor; +#endif + +namespace System.Maui.Platform { + public partial class AbstractViewRenderer : INativeViewRenderer { + + public void SetFrame (Rectangle rect) => View.Frame = rect.ToCGRect (); + + public virtual SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + var s = TypedNativeView.SizeThatFits(new CoreGraphics.CGSize((float)widthConstraint, (float)heightConstraint)); + var request = new Size(s.Width == float.PositiveInfinity ? double.PositiveInfinity : s.Width, + s.Height == float.PositiveInfinity ? double.PositiveInfinity : s.Height); + return new SizeRequest(request); + } + + void SetupContainer() + { + var oldParent = TypedNativeView.Superview; + ContainerView ??= new ContainerView (); + if (oldParent == ContainerView) + return; + ContainerView.MainView = TypedNativeView; + } + void RemoveContainer() + { + + } + + } +} diff --git a/Maui.Core/Renderers/View/AbstractViewRenderer.Standard.cs b/Maui.Core/Renderers/View/AbstractViewRenderer.Standard.cs new file mode 100644 index 000000000000..3bfe91b30958 --- /dev/null +++ b/Maui.Core/Renderers/View/AbstractViewRenderer.Standard.cs @@ -0,0 +1,18 @@ +using System; +using System.Maui.Core; + +namespace System.Maui.Platform +{ + public abstract partial class AbstractViewRenderer + { + public void SetFrame(Rectangle rect) + { + + } + public virtual SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) + => new SizeRequest (); + + void SetupContainer () { } + void RemoveContainer () { } + } +} diff --git a/Maui.Core/Renderers/View/AbstractViewRenderer.Win32.cs b/Maui.Core/Renderers/View/AbstractViewRenderer.Win32.cs new file mode 100644 index 000000000000..200114bce546 --- /dev/null +++ b/Maui.Core/Renderers/View/AbstractViewRenderer.Win32.cs @@ -0,0 +1,34 @@ +using System.Windows; + +namespace System.Maui.Platform +{ + public partial class AbstractViewRenderer : IViewRenderer + { + public void SetFrame(Rectangle rect) + { + TypedNativeView.Arrange(new Rect(rect.Left, rect.Top, rect.Width, rect.Height)); + } + + public virtual SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + if (TypedNativeView == null) + { + return new SizeRequest(Size.Zero); + } + + TypedNativeView.Measure(new Windows.Size(widthConstraint, heightConstraint)); + + var desiredSize = new Size(TypedNativeView.DesiredSize.Width, TypedNativeView.DesiredSize.Height); + + return new SizeRequest(desiredSize); + } + + void SetupContainer() + { + } + + void RemoveContainer() + { + } + } +} diff --git a/Maui.Core/Renderers/View/AbstractViewRenderer.cs b/Maui.Core/Renderers/View/AbstractViewRenderer.cs new file mode 100644 index 000000000000..43b67cde246c --- /dev/null +++ b/Maui.Core/Renderers/View/AbstractViewRenderer.cs @@ -0,0 +1,103 @@ +using System; +using System.Maui.Core; +using System.Maui.Core.Controls; +#if __IOS__ +using NativeView = UIKit.UIView; +#elif __MACOS__ +using NativeView = AppKit.NSView; +#elif MONOANDROID +using NativeView = Android.Views.View; +#elif NETCOREAPP +using NativeView = System.Windows.FrameworkElement; +#elif NETSTANDARD +using NativeView = System.Object; +#endif + +namespace System.Maui.Platform +{ + public abstract partial class AbstractViewRenderer : IViewRenderer + where TVirtualView : class, IFrameworkElement +#if !NETSTANDARD + where TNativeView : NativeView +#endif + { + + protected AbstractViewRenderer (PropertyMapper mapper) + { + this.defaultMapper = mapper; + } + + protected readonly PropertyMapper defaultMapper; + protected PropertyMapper mapper; + + protected abstract TNativeView CreateView(); + + public NativeView View => HasContainer ? (NativeView)ContainerView: TypedNativeView; + public TNativeView TypedNativeView { get; private set; } + protected TVirtualView VirtualView { get; private set; } + public object NativeView => TypedNativeView; + static bool hasSetDefaults; + public virtual void SetView(IFrameworkElement view) + { + VirtualView = view as TVirtualView; + TypedNativeView ??= CreateView(); + if(!hasSetDefaults) + { + SetupDefaults(); + hasSetDefaults = true; + } + mapper = defaultMapper; + if (VirtualView is IPropertyMapperView imv) + { + var map = imv.GetPropertyMapperOverrides(); + var instancePropertyMapper = map as PropertyMapper; + if(map != null && instancePropertyMapper == null) + { + } + if (instancePropertyMapper != null) + { + instancePropertyMapper.Chained = defaultMapper; + mapper = instancePropertyMapper; + } + } + + mapper?.UpdateProperties(this, VirtualView); + } + + public virtual void Remove(IFrameworkElement view) + { + VirtualView = null; + } + + protected virtual void DisposeView(TNativeView nativeView) + { + + } + + public virtual void UpdateValue(string property) + => mapper?.UpdateProperty(this, VirtualView, property); + + protected virtual void SetupDefaults() { } + + public ContainerView ContainerView { get; private set; } + + bool _hasContainer; + public bool HasContainer { + get => _hasContainer; + set { + if (_hasContainer == value) + return; + _hasContainer = value; + if (value) { + SetupContainer (); + _hasContainer = ContainerView != null; + } else + RemoveContainer (); + } + + } + + static protected Color CleanseColor(Color color, Color defaultColor) => color.IsDefault ? defaultColor : color; + + } +} diff --git a/Maui.Core/Renderers/View/ViewRenderer.Android.cs b/Maui.Core/Renderers/View/ViewRenderer.Android.cs new file mode 100644 index 000000000000..8ceb93c11bcf --- /dev/null +++ b/Maui.Core/Renderers/View/ViewRenderer.Android.cs @@ -0,0 +1,23 @@ +using System; +using AView = Android.Views.View; +using Android.Graphics.Drawables; + +namespace System.Maui.Platform { + public partial class ViewRenderer { + public static void MapPropertyIsEnabled (IViewRenderer renderer, IView view) + { + var nativeView = renderer.NativeView as AView; + if (nativeView != null) + nativeView.Enabled = view.IsEnabled; + } + public static void MapBackgroundColor (IViewRenderer renderer, IView view) + { + var aview = renderer.NativeView as AView; + var backgroundColor = view.BackgroundColor; + if (backgroundColor.IsDefault) + aview.Background = null; + else + aview.Background = new ColorDrawable { Color = backgroundColor.ToNative() }; + } + } +} diff --git a/Maui.Core/Renderers/View/ViewRenderer.MaciOS.cs b/Maui.Core/Renderers/View/ViewRenderer.MaciOS.cs new file mode 100644 index 000000000000..fb743ad260e6 --- /dev/null +++ b/Maui.Core/Renderers/View/ViewRenderer.MaciOS.cs @@ -0,0 +1,33 @@ +using System; +#if __MOBILE__ +using NativeColor = UIKit.UIColor; +using NativeControl = UIKit.UIControl; +using NativeView = UIKit.UIView; + +#else +using NativeView = AppKit.NSView; +using NativeColor = CoreGraphics.CGColor; +using NativeControl = AppKit.NSControl; + +#endif + +namespace System.Maui.Platform +{ + public partial class ViewRenderer + { + public static void MapPropertyIsEnabled(IViewRenderer renderer, IView view) + { + var uiControl = renderer.NativeView as NativeControl; + if (uiControl == null) + return; + uiControl.Enabled = view.IsEnabled; + } + public static void MapBackgroundColor (IViewRenderer renderer, IView view) + { + var nativeView = (NativeView)renderer.NativeView; + var color = view.BackgroundColor; + if (color != null && !color.IsDefault) + nativeView.SetBackgroundColor(color.ToNativeColor ()); + } + } +} diff --git a/Maui.Core/Renderers/View/ViewRenderer.Standard.cs b/Maui.Core/Renderers/View/ViewRenderer.Standard.cs new file mode 100644 index 000000000000..997d6dba858e --- /dev/null +++ b/Maui.Core/Renderers/View/ViewRenderer.Standard.cs @@ -0,0 +1,7 @@ +using System; +namespace System.Maui.Platform { + public partial class ViewRenderer { + public static void MapPropertyIsEnabled (IViewRenderer renderer, IView view) { } + public static void MapBackgroundColor (IViewRenderer renderer, IView view) { } + } +} diff --git a/Maui.Core/Renderers/View/ViewRenderer.Win32.cs b/Maui.Core/Renderers/View/ViewRenderer.Win32.cs new file mode 100644 index 000000000000..997d6dba858e --- /dev/null +++ b/Maui.Core/Renderers/View/ViewRenderer.Win32.cs @@ -0,0 +1,7 @@ +using System; +namespace System.Maui.Platform { + public partial class ViewRenderer { + public static void MapPropertyIsEnabled (IViewRenderer renderer, IView view) { } + public static void MapBackgroundColor (IViewRenderer renderer, IView view) { } + } +} diff --git a/Maui.Core/Renderers/View/ViewRenderer.cs b/Maui.Core/Renderers/View/ViewRenderer.cs new file mode 100644 index 000000000000..d4705f59cf99 --- /dev/null +++ b/Maui.Core/Renderers/View/ViewRenderer.cs @@ -0,0 +1,29 @@ +using System; + + +namespace System.Maui.Platform { + public partial class ViewRenderer { + public static PropertyMapper ViewMapper = new PropertyMapper { + [nameof(IView.IsEnabled)] = MapPropertyIsEnabled, + [nameof(IView.BackgroundColor)] = MapBackgroundColor, + [nameof(IView.Frame)] = MapPropertyFrame, + [nameof(IClipShapeView.ClipShape)] = MapPropertyClipShape + }; + + public static void MapPropertyFrame(IViewRenderer renderer, IView view) + => renderer?.SetFrame(view.Frame); + + + public static void MapPropertyClipShape(IViewRenderer renderer, IView view) + { + if (!(view is IClipShapeView clipShape)) + return; + //If we are ever going to set HasContainer = false, + //we need to add a method to verify if anything else requires it + if (clipShape.ClipShape != null) + renderer.HasContainer = true; + if (renderer.ContainerView != null) + renderer.ContainerView.ClipShape = clipShape.ClipShape; + } + } +} diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.Android.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.Android.cs new file mode 100644 index 000000000000..a719606e677c --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.Android.cs @@ -0,0 +1,183 @@ +using System.Threading.Tasks; +using Android.Webkit; +using Android.Widget; +using static Android.Views.ViewGroup; +using AWebView = Android.Webkit.WebView; + +namespace System.Maui.Platform +{ + public partial class WebViewRenderer : AbstractViewRenderer, IWebViewDelegate + { + public const string AssetBaseUrl = "file:///android_asset/"; + + // readonly bool _ignoreSourceChanges; + WebNavigationEvent _eventState; + WebViewClient _webViewClient; + WebChromeClient _webChromeClient; + + protected internal string UrlCanceled { get; set; } + + protected override AWebView CreateView() + { + var aWebView = new AWebView(Context) + { +#pragma warning disable 618 // This can probably be replaced with LinearLayout(LayoutParams.MatchParent, LayoutParams.MatchParent); just need to test that theory + LayoutParameters = new AbsoluteLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent, 0, 0) +#pragma warning restore 618 + }; + + aWebView.Settings.JavaScriptEnabled = true; + aWebView.Settings.DomStorageEnabled = true; + + _webViewClient = GetWebViewClient(); + aWebView.SetWebViewClient(_webViewClient); + + _webChromeClient = GetWebChromeClient(); + aWebView.SetWebChromeClient(_webChromeClient); + + VirtualView.EvalRequested += OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested += OnGoBackRequested; + VirtualView.GoForwardRequested += OnGoForwardRequested; + VirtualView.ReloadRequested += OnReloadRequested; + + return aWebView; + } + + protected override void DisposeView(AWebView aWebView) + { + VirtualView.EvalRequested -= OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested -= OnGoBackRequested; + VirtualView.GoForwardRequested -= OnGoForwardRequested; + VirtualView.ReloadRequested -= OnReloadRequested; + + aWebView.StopLoading(); + + _webViewClient?.Dispose(); + _webChromeClient?.Dispose(); + + base.DisposeView(aWebView); + } + + public static void MapPropertySource(IViewRenderer renderer, IWebView webView) + { + (renderer as WebViewRenderer)?.Load(); + } + + public void LoadHtml(string html, string baseUrl) + { + _eventState = WebNavigationEvent.NewPage; + TypedNativeView.LoadDataWithBaseURL(baseUrl ?? AssetBaseUrl, html, "text/html", "UTF-8", null); + } + + public void LoadUrl(string url) + { + if (!SendNavigatingCanceled(url)) + { + _eventState = WebNavigationEvent.NewPage; + TypedNativeView.LoadUrl(url); + } + } + + protected virtual WebViewClient GetWebViewClient() + { + return new WebViewClient(); + } + + protected virtual WebChromeClient GetWebChromeClient() + { + return new WebChromeClient(); + } + + protected internal void UpdateCanGoBackForward() + { + VirtualView.CanGoBack = TypedNativeView.CanGoBack(); + VirtualView.CanGoForward = TypedNativeView.CanGoForward(); + } + + protected internal bool SendNavigatingCanceled(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return true; + + if (url == AssetBaseUrl) + return false; + + var args = new WebNavigatingEventArgs(_eventState, new UrlWebViewSource { Url = url }, url); + VirtualView.Navigating(args); + UpdateCanGoBackForward(); + UrlCanceled = args.Cancel ? null : url; + return args.Cancel; + } + + void Load() + { + //if (_ignoreSourceChanges) + // return; + + VirtualView.Source?.Load(this); + + UpdateCanGoBackForward(); + } + + void OnEvalRequested(object sender, EvalRequested eventArg) + { + LoadUrl("javascript:" + eventArg.Script); + } + + async Task OnEvaluateJavaScriptRequested(string script) + { + var jsr = new JavascriptResult(); + + TypedNativeView.EvaluateJavascript(script, jsr); + + return await jsr.JsResult.ConfigureAwait(false); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoBack()) + { + _eventState = WebNavigationEvent.Back; + TypedNativeView.GoBack(); + } + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoForward()) + { + _eventState = WebNavigationEvent.Forward; + TypedNativeView.GoForward(); + } + + UpdateCanGoBackForward(); + } + + void OnReloadRequested(object sender, EventArgs eventArgs) + { + _eventState = WebNavigationEvent.Refresh; + TypedNativeView.Reload(); + } + + class JavascriptResult : Java.Lang.Object, IValueCallback + { + readonly TaskCompletionSource _source; + public Task JsResult { get { return _source.Task; } } + + public JavascriptResult() + { + _source = new TaskCompletionSource(); + } + + public void OnReceiveValue(Java.Lang.Object result) + { + string json = ((Java.Lang.String)result).ToString(); + _source.SetResult(json); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.Mac.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.Mac.cs new file mode 100644 index 000000000000..3fb87d06833b --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.Mac.cs @@ -0,0 +1,151 @@ +using System.Threading.Tasks; +using AppKit; +using Foundation; +using WebKit; + +namespace System.Maui.Platform +{ + public partial class WebViewRenderer : AbstractViewRenderer, IWebViewDelegate + { + bool _ignoreSourceChanges; + WebNavigationEvent _lastBackForwardEvent; + WebNavigationEvent _lastEvent; + + protected override WebView CreateView() + { + var webView = new WebView + { + AutoresizingMask = NSViewResizingMask.WidthSizable, + AutoresizesSubviews = true + }; + + webView.FrameLoadDelegate = new MauiWebFrameDelegate(this); + + return webView; + } + + protected override void DisposeView(WebView webView) + { + VirtualView.EvalRequested -= OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested -= OnGoBackRequested; + VirtualView.GoForwardRequested -= OnGoForwardRequested; + VirtualView.ReloadRequested -= OnReloadRequested; + + base.DisposeView(webView); + } + + public static void MapPropertySource(IViewRenderer renderer, IWebView webView) + { + (renderer as WebViewRenderer)?.Load(); + } + + public void LoadHtml(string html, string baseUrl) + { + if (html != null) + TypedNativeView.MainFrame.LoadHtmlString(html, + baseUrl == null ? new NSUrl(NSBundle.MainBundle.BundlePath, true) : new NSUrl(baseUrl, true)); + } + + public void LoadUrl(string url) + { + TypedNativeView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(url))); + } + + protected internal void UpdateCanGoBackForward() + { + VirtualView.CanGoBack = TypedNativeView.CanGoBack(); + VirtualView.CanGoForward = TypedNativeView.CanGoForward(); + } + + void Load() + { + if (_ignoreSourceChanges) + return; + + VirtualView?.Source?.Load(this); + + UpdateCanGoBackForward(); + } + + void OnEvalRequested(object sender, EvalRequested eventArg) + { + TypedNativeView?.StringByEvaluatingJavaScriptFromString(eventArg?.Script); + } + + async Task OnEvaluateJavaScriptRequested(string script) + { + var tcr = new TaskCompletionSource(); + + var task = tcr.Task; + + tcr.SetResult(TypedNativeView?.StringByEvaluatingJavaScriptFromString(script)); + + return await task.ConfigureAwait(false); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoBack()) + { + _lastBackForwardEvent = WebNavigationEvent.Back; + TypedNativeView.GoBack(); + } + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoForward()) + { + _lastBackForwardEvent = WebNavigationEvent.Forward; + TypedNativeView.GoForward(); + } + + UpdateCanGoBackForward(); + } + + void OnReloadRequested(object sender, EventArgs eventArgs) + { + TypedNativeView.Reload(TypedNativeView); + } + + internal class MauiWebFrameDelegate : WebFrameLoadDelegate + { + readonly WebViewRenderer _webViewRenderer; + + internal MauiWebFrameDelegate(WebViewRenderer webViewRenderer) + { + _webViewRenderer = webViewRenderer; + } + + public override void FinishedLoad(WebView sender, WebFrame forFrame) + { + if (_webViewRenderer.TypedNativeView.IsLoading) + return; + + if (_webViewRenderer.TypedNativeView.MainFrameUrl == $"file://{NSBundle.MainBundle.BundlePath}/") + return; + + _webViewRenderer._ignoreSourceChanges = true; + _webViewRenderer.VirtualView.Source = new UrlWebViewSource { Url = _webViewRenderer.TypedNativeView.MainFrameUrl }; + _webViewRenderer._ignoreSourceChanges = false; + + _webViewRenderer._lastEvent = _webViewRenderer._lastBackForwardEvent; + _webViewRenderer.VirtualView?.Navigated(new WebNavigatedEventArgs(_webViewRenderer._lastEvent, _webViewRenderer.VirtualView?.Source, _webViewRenderer.TypedNativeView.MainFrameUrl, WebNavigationResult.Success)); + + _webViewRenderer.UpdateCanGoBackForward(); + } + + public override void FailedLoadWithError(WebView sender, NSError error, WebFrame forFrame) + { + _webViewRenderer._lastEvent = _webViewRenderer._lastBackForwardEvent; + + _webViewRenderer.VirtualView?.Navigated(new WebNavigatedEventArgs(_webViewRenderer._lastEvent, new UrlWebViewSource { Url = _webViewRenderer.TypedNativeView.MainFrameUrl }, _webViewRenderer.TypedNativeView.MainFrameUrl, WebNavigationResult.Failure)); + + _webViewRenderer.UpdateCanGoBackForward(); + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.Standard.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.Standard.cs new file mode 100644 index 000000000000..4d41e6fcfd60 --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.Standard.cs @@ -0,0 +1,9 @@ +namespace System.Maui.Platform +{ + public partial class WebViewRenderer : AbstractViewRenderer + { + protected override object CreateView() => throw new NotImplementedException(); + + public static void MapPropertySource(IViewRenderer renderer, IWebView webView) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.Win32.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.Win32.cs new file mode 100644 index 000000000000..a360289b07f3 --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.Win32.cs @@ -0,0 +1,162 @@ +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Navigation; +using System.Windows.Threading; + +namespace System.Maui.Platform +{ + public partial class WebViewRenderer : AbstractViewRenderer, IWebViewDelegate + { + WebNavigationEvent _eventState; + bool _updating; + + protected override WebBrowser CreateView() + { + var webBrowser = new WebBrowser(); + + webBrowser.Navigated += WebBrowserOnNavigated; + webBrowser.Navigating += WebBrowserOnNavigating; + + VirtualView.EvalRequested += OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested += OnGoBackRequested; + VirtualView.GoForwardRequested += OnGoForwardRequested; + VirtualView.ReloadRequested += OnReloadRequested; + + return webBrowser; + } + + protected override void DisposeView(WebBrowser webBrowser) + { + webBrowser.Navigated -= WebBrowserOnNavigated; + webBrowser.Navigating -= WebBrowserOnNavigating; + + VirtualView.EvalRequested -= OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested -= OnGoBackRequested; + VirtualView.GoForwardRequested -= OnGoForwardRequested; + VirtualView.ReloadRequested -= OnReloadRequested; + + base.DisposeView(webBrowser); + } + + public static void MapPropertySource(IViewRenderer renderer, IWebView webView) + { + (renderer as WebViewRenderer)?.Load(); + } + + public void LoadHtml(string html, string baseUrl) + { + if (html == null) + return; + + TypedNativeView.NavigateToString(html); + } + + public void LoadUrl(string url) + { + if (url == null) + return; + + TypedNativeView.Source = new Uri(url, UriKind.RelativeOrAbsolute); + } + + protected internal void UpdateCanGoBackForward() + { + VirtualView.CanGoBack = TypedNativeView.CanGoBack; + VirtualView.CanGoForward = TypedNativeView.CanGoForward; + } + + void Load() + { + if (_updating) + return; + + if (VirtualView.Source != null) + VirtualView.Source.Load(this); + + UpdateCanGoBackForward(); + } + + void WebBrowserOnNavigated(object sender, NavigationEventArgs navigationEventArgs) + { + if (navigationEventArgs.Uri == null) + return; + + string url = navigationEventArgs.Uri.IsAbsoluteUri ? navigationEventArgs.Uri.AbsoluteUri : navigationEventArgs.Uri.OriginalString; + SendNavigated(new UrlWebViewSource { Url = url }, _eventState, WebNavigationResult.Success); + UpdateCanGoBackForward(); + } + + void WebBrowserOnNavigating(object sender, NavigatingCancelEventArgs navigatingEventArgs) + { + if (navigatingEventArgs.Uri == null) + return; + + string url = navigatingEventArgs.Uri.IsAbsoluteUri ? navigatingEventArgs.Uri.AbsoluteUri : navigatingEventArgs.Uri.OriginalString; + var args = new WebNavigatingEventArgs(_eventState, new UrlWebViewSource { Url = url }, url); + + VirtualView.Navigating(args); + + navigatingEventArgs.Cancel = args.Cancel; + + // Reset in this case because this is the last event we will get + if (args.Cancel) + _eventState = WebNavigationEvent.NewPage; + } + + void OnEvalRequested(object sender, EvalRequested eventArg) + { + TypedNativeView.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => TypedNativeView.InvokeScript("eval", eventArg.Script))); + } + + async Task OnEvaluateJavaScriptRequested(string script) + { + var tcr = new TaskCompletionSource(); + + var task = tcr.Task; + + tcr.SetResult((string)TypedNativeView.InvokeScript("eval", new[] { script })); + + return await task.ConfigureAwait(false); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoBack) + { + _eventState = WebNavigationEvent.Back; + TypedNativeView.GoBack(); + } + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoForward) + { + _eventState = WebNavigationEvent.Forward; + TypedNativeView.GoForward(); + } + UpdateCanGoBackForward(); + } + + void OnReloadRequested(object sender, EventArgs eventArgs) + { + TypedNativeView.Refresh(); + } + + void SendNavigated(UrlWebViewSource source, WebNavigationEvent evnt, WebNavigationResult result) + { + _updating = true; + VirtualView.Source = source; + _updating = false; + + VirtualView.Navigated(new WebNavigatedEventArgs(evnt, source, source.Url, result)); + + UpdateCanGoBackForward(); + _eventState = WebNavigationEvent.NewPage; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.cs new file mode 100644 index 000000000000..f004ba279310 --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.cs @@ -0,0 +1,24 @@ +namespace System.Maui.Platform +{ + public partial class WebViewRenderer + { + public static PropertyMapper WebViewMapper = new PropertyMapper(ViewRenderer.ViewMapper) + { + [nameof(IWebView.Source)] = MapPropertySource, + Actions = { + ["GoBack"] = MapGoBack + } + }; + + public WebViewRenderer() : base(WebViewMapper) + { + + } + + public WebViewRenderer(PropertyMapper mapper) : base(mapper ?? WebViewMapper) + { + + } + public static void MapGoBack(IViewRenderer renderer, IWebView webView) { } + } +} \ No newline at end of file diff --git a/Maui.Core/Renderers/WebView/WebViewRenderer.iOS.cs b/Maui.Core/Renderers/WebView/WebViewRenderer.iOS.cs new file mode 100644 index 000000000000..fb472544d988 --- /dev/null +++ b/Maui.Core/Renderers/WebView/WebViewRenderer.iOS.cs @@ -0,0 +1,312 @@ +using System.Maui.Core.Platform; +using System.Threading.Tasks; +using Foundation; +using UIKit; +using WebKit; +using RectangleF = CoreGraphics.CGRect; + +namespace System.Maui.Platform +{ + public partial class WebViewRenderer : AbstractViewRenderer, IWebViewDelegate + { + protected bool _ignoreSourceChanges; + protected WebNavigationEvent _lastBackForwardEvent; + + protected override WKWebView CreateView() + { + var wKWebView = new WKWebView(RectangleF.Empty, new WKWebViewConfiguration()) + { + NavigationDelegate = new CustomWebViewNavigationDelegate(this), + UIDelegate = new CustomWebViewUIDelegate() + }; + + VirtualView.EvalRequested += OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested += OnGoBackRequested; + VirtualView.GoForwardRequested += OnGoForwardRequested; + VirtualView.ReloadRequested += OnReloadRequested; + + return wKWebView; + } + + protected override void DisposeView(WKWebView wKWebView) + { + VirtualView.EvalRequested -= OnEvalRequested; + VirtualView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested; + VirtualView.GoBackRequested -= OnGoBackRequested; + VirtualView.GoForwardRequested -= OnGoForwardRequested; + VirtualView.ReloadRequested -= OnReloadRequested; + + base.DisposeView(wKWebView); + } + + public static void MapPropertySource(IViewRenderer renderer, IWebView webView) + { + (renderer as WebViewRenderer)?.Load(); + } + + public void LoadHtml(string html, string baseUrl) + { + if (html != null) + TypedNativeView.LoadHtmlString(html, baseUrl == null ? new NSUrl(NSBundle.MainBundle.BundlePath, true) : new NSUrl(baseUrl, true)); + } + + public void LoadUrl(string url) + { + var uri = new Uri(url); + var safeHostUri = new Uri($"{uri.Scheme}://{uri.Authority}", UriKind.Absolute); + var safeRelativeUri = new Uri($"{uri.PathAndQuery}{uri.Fragment}", UriKind.Relative); + NSUrlRequest request = new NSUrlRequest(new Uri(safeHostUri, safeRelativeUri)); + + TypedNativeView.LoadRequest(request); + } + + protected internal void UpdateCanGoBackForward() + { + VirtualView.CanGoBack = TypedNativeView.CanGoBack; + VirtualView.CanGoForward = TypedNativeView.CanGoForward; + } + + void Load() + { + if (_ignoreSourceChanges) + return; + + if (VirtualView.Source != null) + VirtualView.Source.Load(this); + + UpdateCanGoBackForward(); + } + + void OnEvalRequested(object sender, EvalRequested eventArg) + { + TypedNativeView.EvaluateJavaScriptAsync(eventArg.Script); + } + + async Task OnEvaluateJavaScriptRequested(string script) + { + var result = await TypedNativeView.EvaluateJavaScriptAsync(script); + return result?.ToString(); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoBack) + { + _lastBackForwardEvent = WebNavigationEvent.Back; + TypedNativeView.GoBack(); + } + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (TypedNativeView.CanGoForward) + { + _lastBackForwardEvent = WebNavigationEvent.Forward; + TypedNativeView.GoForward(); + } + + UpdateCanGoBackForward(); + } + + void OnReloadRequested(object sender, EventArgs eventArgs) + { + TypedNativeView.Reload(); + } + + class CustomWebViewNavigationDelegate : WKNavigationDelegate + { + readonly WebViewRenderer _webViewRenderer; + WebNavigationEvent _lastEvent; + + public CustomWebViewNavigationDelegate(WebViewRenderer webViewRenderer) + { + _webViewRenderer = webViewRenderer ?? throw new ArgumentNullException("renderer"); + } + + IWebView WebView => _webViewRenderer.VirtualView; + + public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) + { + var url = GetCurrentUrl(); + WebView.Navigated( + new WebNavigatedEventArgs(_lastEvent, new UrlWebViewSource { Url = url }, url, WebNavigationResult.Failure) + ); + + _webViewRenderer.UpdateCanGoBackForward(); + } + + public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) + { + if (webView.IsLoading) + return; + + var url = GetCurrentUrl(); + if (url == $"file://{NSBundle.MainBundle.BundlePath}/") + return; + + _webViewRenderer._ignoreSourceChanges = true; + WebView.Source = new UrlWebViewSource { Url = url }; + _webViewRenderer._ignoreSourceChanges = false; + + var args = new WebNavigatedEventArgs(_lastEvent, WebView.Source, url, WebNavigationResult.Success); + WebView.Navigated(args); + + _webViewRenderer.UpdateCanGoBackForward(); + + } + + public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) + { + } + + // https://stackoverflow.com/questions/37509990/migrating-from-uiwebview-to-wkwebview + public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) + { + var navEvent = WebNavigationEvent.NewPage; + var navigationType = navigationAction.NavigationType; + switch (navigationType) + { + case WKNavigationType.LinkActivated: + navEvent = WebNavigationEvent.NewPage; + + if (navigationAction.TargetFrame == null) + webView?.LoadRequest(navigationAction.Request); + + break; + case WKNavigationType.FormSubmitted: + navEvent = WebNavigationEvent.NewPage; + break; + case WKNavigationType.BackForward: + navEvent = _webViewRenderer._lastBackForwardEvent; + break; + case WKNavigationType.Reload: + navEvent = WebNavigationEvent.Refresh; + break; + case WKNavigationType.FormResubmitted: + navEvent = WebNavigationEvent.NewPage; + break; + case WKNavigationType.Other: + navEvent = WebNavigationEvent.NewPage; + break; + } + + _lastEvent = navEvent; + var request = navigationAction.Request; + var lastUrl = request.Url.ToString(); + var args = new WebNavigatingEventArgs(navEvent, new UrlWebViewSource { Url = lastUrl }, lastUrl); + + WebView.Navigating(args); + _webViewRenderer.UpdateCanGoBackForward(); + decisionHandler(args.Cancel ? WKNavigationActionPolicy.Cancel : WKNavigationActionPolicy.Allow); + } + + string GetCurrentUrl() + { + return _webViewRenderer?.TypedNativeView?.Url?.AbsoluteUrl?.ToString(); + } + } + + class CustomWebViewUIDelegate : WKUIDelegate + { + static readonly string LocalOK = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("OK"); + static readonly string LocalCancel = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("Cancel"); + + public override void RunJavaScriptAlertPanel(WKWebView webView, string message, WKFrameInfo frame, Action completionHandler) + { + PresentAlertController( + webView, + message, + okAction: _ => completionHandler() + ); + } + + public override void RunJavaScriptConfirmPanel(WKWebView webView, string message, WKFrameInfo frame, Action completionHandler) + { + PresentAlertController( + webView, + message, + okAction: _ => completionHandler(true), + cancelAction: _ => completionHandler(false) + ); + } + + public override void RunJavaScriptTextInputPanel( + WKWebView webView, string prompt, string defaultText, WKFrameInfo frame, Action completionHandler) + { + PresentAlertController( + webView, + prompt, + defaultText: defaultText, + okAction: x => completionHandler(x.TextFields[0].Text), + cancelAction: _ => completionHandler(null) + ); + } + + static string GetJsAlertTitle(WKWebView webView) + { + // Emulate the behavior of UIWebView dialogs. + // The scheme and host are used unless local html content is what the webview is displaying, + // in which case the bundle file name is used. + + if (webView.Url != null && webView.Url.AbsoluteString != $"file://{NSBundle.MainBundle.BundlePath}/") + return $"{webView.Url.Scheme}://{webView.Url.Host}"; + + return new NSString(NSBundle.MainBundle.BundlePath).LastPathComponent; + } + + static UIAlertAction AddOkAction(UIAlertController controller, Action handler) + { + var action = UIAlertAction.Create(LocalOK, UIAlertActionStyle.Default, (_) => handler()); + controller.AddAction(action); + controller.PreferredAction = action; + return action; + } + + static UIAlertAction AddCancelAction(UIAlertController controller, Action handler) + { + var action = UIAlertAction.Create(LocalCancel, UIAlertActionStyle.Cancel, (_) => handler()); + controller.AddAction(action); + return action; + } + + static void PresentAlertController( + WKWebView webView, + string message, + string defaultText = null, + Action okAction = null, + Action cancelAction = null) + { + var controller = UIAlertController.Create(GetJsAlertTitle(webView), message, UIAlertControllerStyle.Alert); + + if (defaultText != null) + controller.AddTextField((textField) => textField.Text = defaultText); + + if (okAction != null) + AddOkAction(controller, () => okAction(controller)); + + if (cancelAction != null) + AddCancelAction(controller, () => cancelAction(controller)); + + GetTopViewController(UIApplication.SharedApplication.GetKeyWindow().RootViewController) + .PresentViewController(controller, true, null); + } + + static UIViewController GetTopViewController(UIViewController viewController) + { + if (viewController is UINavigationController navigationController) + return GetTopViewController(navigationController.VisibleViewController); + + if (viewController is UITabBarController tabBarController) + return GetTopViewController(tabBarController.SelectedViewController); + + if (viewController.PresentedViewController != null) + return GetTopViewController(viewController.PresentedViewController); + + return viewController; + } + } + } +} \ No newline at end of file diff --git a/Maui.Core/Resources/Layout/content_main.xml b/Maui.Core/Resources/Layout/content_main.xml new file mode 100644 index 000000000000..33bd48841862 --- /dev/null +++ b/Maui.Core/Resources/Layout/content_main.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Maui.Core/Shapes/Capsule.cs b/Maui.Core/Shapes/Capsule.cs new file mode 100644 index 000000000000..dc53688198bd --- /dev/null +++ b/Maui.Core/Shapes/Capsule.cs @@ -0,0 +1,21 @@ +using System; +using System.Drawing; +using System.Maui.Graphics; + +namespace System.Maui.Shapes +{ + /// + /// A capsule shape is equivalent to a rounded rectangle where the corner radius is chosen + /// as half the length of the rectangle’s smallest edge. + /// + public class Capsule : IShape + { + public Path PathForBounds(Maui.Rectangle rect) + { + var path = new Path(); + var cornerSize = Math.Min(rect.Width, rect.Height) / 2; + path.AppendRoundedRectangle(rect, cornerSize); + return path; + } + } +} diff --git a/Maui.Core/Shapes/Circle.cs b/Maui.Core/Shapes/Circle.cs new file mode 100644 index 000000000000..98076dea6125 --- /dev/null +++ b/Maui.Core/Shapes/Circle.cs @@ -0,0 +1,19 @@ +using System; +using System.Drawing; +using System.Maui.Graphics; + +namespace System.Maui.Shapes +{ + public class Circle : IShape + { + public Path PathForBounds(Maui.Rectangle rect) + { + var size = Math.Min(rect.Width, rect.Height); + var x = rect.X + (rect.Width - size) / 2; + var y = rect.Y + (rect.Height - size) / 2; + var path = new Path(); + path.AppendEllipse(x, y, size, size); + return path; + } + } +} diff --git a/Maui.Core/Shapes/Ellipse.cs b/Maui.Core/Shapes/Ellipse.cs new file mode 100644 index 000000000000..a2745e693f08 --- /dev/null +++ b/Maui.Core/Shapes/Ellipse.cs @@ -0,0 +1,14 @@ +using System.Maui.Graphics; + +namespace System.Maui.Shapes +{ + public class Ellipse : IShape + { + public Path PathForBounds(Maui.Rectangle rect) + { + var path = new Path(); + path.AppendEllipse(rect); + return path; + } + } +} diff --git a/Maui.Core/Shapes/IShape.cs b/Maui.Core/Shapes/IShape.cs new file mode 100644 index 000000000000..dd528f8027e2 --- /dev/null +++ b/Maui.Core/Shapes/IShape.cs @@ -0,0 +1,10 @@ +using System; +using System.Maui.Graphics; + +namespace System.Maui.Shapes +{ + public interface IShape + { + Path PathForBounds(Maui.Rectangle rect); + } +} diff --git a/Maui.Core/Shapes/Path.cs b/Maui.Core/Shapes/Path.cs new file mode 100644 index 000000000000..35acb47a57cc --- /dev/null +++ b/Maui.Core/Shapes/Path.cs @@ -0,0 +1,85 @@ +using System; +using System.Maui.Graphics; + +namespace System.Maui.Shapes { + public class PathShape : IShape + { + private readonly Path _path; + private readonly PathScaling _scaling; + + public PathShape(Path path, PathScaling scaling = PathScaling.AspectFit) + { + _path = path; + _scaling = scaling; + } + + public PathShape(string path, PathScaling scaling = PathScaling.AspectFit) + { + _path = PathBuilder.Build(path); + _scaling = scaling; + } + + public Path PathForBounds(Rectangle rect) + { + var bounds = _path.Bounds; + + AffineTransformF transform = null; + + if (_scaling == PathScaling.AspectFit) + { + var factorX = rect.Width / bounds.Width; + var factorY = rect.Height / bounds.Height; + var factor = Math.Min(factorX, factorY); + + var width = bounds.Width * factor; + var height = bounds.Height * factor; + var translateX = (rect.Width - width) / 2; + var translateY = (rect.Height - height) / 2; + + transform = AffineTransformF.GetTranslateInstance(-bounds.X, -bounds.Y); + transform.Translate(translateX, translateY); + transform.Scale(factor, factor); + } + else if (_scaling == PathScaling.AspectFill) + { + var factorX = rect.Width / bounds.Width; + var factorY = rect.Height / bounds.Height; + var factor = Math.Max(factorX, factorY); + + var width = bounds.Width * factor; + var height = bounds.Height * factor; + var translateX = (rect.Width - width) / 2; + var translateY = (rect.Height - height) / 2; + + transform = AffineTransformF.GetTranslateInstance(-bounds.X, -bounds.Y); + transform.Translate(translateX, translateY); + transform.Scale(factor, factor); + } + else if (_scaling == PathScaling.Fill) + { + var factorX = rect.Width / bounds.Width; + var factorY = rect.Height / bounds.Height; + transform = AffineTransformF.GetScaleInstance(factorX, factorY); + + var translateX = bounds.X * factorX; + var translateY = bounds.Y * factorY; + transform.Translate(translateX, translateY); + } + else + { + var width = bounds.Width; + var height = bounds.Height; + var translateX = (rect.Width - width) / 2; + var translateY = (rect.Height - height) / 2; + + transform = AffineTransformF.GetTranslateInstance(-bounds.X, -bounds.Y); + transform.Translate(translateX, translateY); + } + + if (!transform?.IsIdentity ?? false) + return _path.Transform(transform); + + return _path; + } + } +} diff --git a/Maui.Core/Shapes/Pill.cs b/Maui.Core/Shapes/Pill.cs new file mode 100644 index 000000000000..a7cb7bd93113 --- /dev/null +++ b/Maui.Core/Shapes/Pill.cs @@ -0,0 +1,22 @@ +//using System; +//using System.Maui.Graphics; + +//namespace System.Maui.Shapes { +// public class Pill : IShape +// { +// public Pill(Orientation orientation) +// { +// Orientation = orientation; +// } + +// public Orientation Orientation { get; } + +// public Path PathForBounds (Maui.Rectangle rect) +// { +// var cornerRadius = (Orientation == Orientation.Horizontal ? rect.Height : rect.Width) / 2f; +// var path = new Path(); +// path.AppendRoundedRectangle(rect, cornerRadius); +// return path; +// } +// } +//} diff --git a/Maui.Core/Shapes/Rectangle.cs b/Maui.Core/Shapes/Rectangle.cs new file mode 100644 index 000000000000..c3dd5ef85d71 --- /dev/null +++ b/Maui.Core/Shapes/Rectangle.cs @@ -0,0 +1,14 @@ +using System.Maui.Graphics; + +namespace System.Maui.Shapes +{ + public class RectangleShape : IShape + { + public Path PathForBounds (Maui.Rectangle rect) + { + var path = new Path(); + path.AppendRectangle(rect); + return path; + } + } +} diff --git a/Maui.Core/Shapes/RoundedRectangle.cs b/Maui.Core/Shapes/RoundedRectangle.cs new file mode 100644 index 000000000000..ea046d9913ba --- /dev/null +++ b/Maui.Core/Shapes/RoundedRectangle.cs @@ -0,0 +1,27 @@ +using System.Maui.Graphics; +using System.Maui.Core; + +namespace System.Maui.Shapes +{ + public class RoundedRectangle : IShape + { + public RoundedRectangle(float cornerRadius) + { + CornerRadius = cornerRadius; + } + + public RoundedRectangle(float cornerRadiusTopLeft, float cornerRadiusTopRight, float cornerRadiusBottomRight, float cornerRadiusBottomLeft) + { + + } + + public float CornerRadius { get; set; } + + public Path PathForBounds(System.Maui.Rectangle rect) + { + var path = new Path(); + path.AppendRoundedRectangle(rect, CornerRadius); + return path; + } + } +} diff --git a/Maui.Core/Views/IActivityIndicator.cs b/Maui.Core/Views/IActivityIndicator.cs new file mode 100644 index 000000000000..f9c99e00e7db --- /dev/null +++ b/Maui.Core/Views/IActivityIndicator.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface IActivityIndicator : IView + { + bool IsRunning { get; } + Color Color { get; } + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IBorder.cs b/Maui.Core/Views/IBorder.cs new file mode 100644 index 000000000000..9d2b012b0df1 --- /dev/null +++ b/Maui.Core/Views/IBorder.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface IBorder + { + Color BorderColor { get; } + double BorderWidth { get; } + } +} diff --git a/Maui.Core/Views/IButton.cs b/Maui.Core/Views/IButton.cs new file mode 100644 index 000000000000..73d22dd024ba --- /dev/null +++ b/Maui.Core/Views/IButton.cs @@ -0,0 +1,6 @@ +using System; +namespace System.Maui { + public interface IButton : IText { + void Clicked (); + } +} diff --git a/Maui.Core/Views/ICheckBox.cs b/Maui.Core/Views/ICheckBox.cs new file mode 100644 index 000000000000..e2b614823750 --- /dev/null +++ b/Maui.Core/Views/ICheckBox.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface ICheckBox : IView + { + bool IsChecked { get; set; } + Color Color { get; } + } +} diff --git a/Maui.Core/Views/IClipShapeView.cs b/Maui.Core/Views/IClipShapeView.cs new file mode 100644 index 000000000000..fbbdb0586943 --- /dev/null +++ b/Maui.Core/Views/IClipShapeView.cs @@ -0,0 +1,9 @@ +using System.Maui.Shapes; + +namespace System.Maui +{ + public interface IClipShapeView + { + IShape ClipShape { get; } + } +} diff --git a/Maui.Core/Views/IDatePicker.cs b/Maui.Core/Views/IDatePicker.cs new file mode 100644 index 000000000000..7c849ee69404 --- /dev/null +++ b/Maui.Core/Views/IDatePicker.cs @@ -0,0 +1,11 @@ +namespace System.Maui +{ + public interface IDatePicker : IText + { + DateTime SelectedDate { get; set; } + DateTime MinimumDate { get; } + DateTime MaximumDate { get; } + + string IText.Text => SelectedDate.ToShortDateString(); + } +} diff --git a/Maui.Core/Views/IEditor.cs b/Maui.Core/Views/IEditor.cs new file mode 100644 index 000000000000..20ce2850e961 --- /dev/null +++ b/Maui.Core/Views/IEditor.cs @@ -0,0 +1,9 @@ +namespace System.Maui +{ + public interface IEditor : ITextInput + { + public EditorAutoSizeOption AutoSize { get; } + + void Completed(); + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IExpander.cs b/Maui.Core/Views/IExpander.cs new file mode 100644 index 000000000000..f833e7f53090 --- /dev/null +++ b/Maui.Core/Views/IExpander.cs @@ -0,0 +1,10 @@ +namespace System.Maui +{ + public interface IExpander : IView + { + double Spacing { get; } + IView Header { get; } + IView Content { get; } + bool IsExpanded { get; set; } + } +} diff --git a/Maui.Core/Views/IFrameworkElement.cs b/Maui.Core/Views/IFrameworkElement.cs new file mode 100644 index 000000000000..80a04cb053f4 --- /dev/null +++ b/Maui.Core/Views/IFrameworkElement.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui +{ + public interface IFrameworkElement + { + bool IsEnabled { get; } + Color BackgroundColor { get; } + Rectangle Frame { get; } + IViewRenderer Renderer { get; set; } + IFrameworkElement Parent { get; } + + void Arrange(Rectangle bounds); + SizeRequest Measure(double widthConstraint, double heightConstraint); + + SizeRequest DesiredSize { get; } + bool IsMeasureValid { get; } + bool IsArrangeValid { get; } + + void InvalidateMeasure(); + void InvalidateArrange(); + } +} diff --git a/Maui.Core/Views/ILabel.cs b/Maui.Core/Views/ILabel.cs new file mode 100644 index 000000000000..c6635032310d --- /dev/null +++ b/Maui.Core/Views/ILabel.cs @@ -0,0 +1,7 @@ +namespace System.Maui +{ + public interface ILabel : IText + { + double LineHeight { get; } + } +} diff --git a/Maui.Core/Views/ILayout.cs b/Maui.Core/Views/ILayout.cs new file mode 100644 index 000000000000..28528621a0a2 --- /dev/null +++ b/Maui.Core/Views/ILayout.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace System.Maui +{ + public interface ILayout : IView + { + IList Children { get; } + } + + public interface IStackLayout : ILayout + { + Orientation Orientation { get; } + } + + public enum Alignment + { + Start, + Center, + End, + Fill + } + + public enum Orientation + { + Vertical, + Horizontal + } +} diff --git a/Maui.Core/Views/IPage.cs b/Maui.Core/Views/IPage.cs new file mode 100644 index 000000000000..02f68d06039e --- /dev/null +++ b/Maui.Core/Views/IPage.cs @@ -0,0 +1,9 @@ +namespace System.Maui +{ + public interface IPage : IFrameworkElement + { + string Title { get; } + + object Content { get; } + } +} diff --git a/Maui.Core/Views/IPicker.cs b/Maui.Core/Views/IPicker.cs new file mode 100644 index 000000000000..cd9fe9ab4d58 --- /dev/null +++ b/Maui.Core/Views/IPicker.cs @@ -0,0 +1,16 @@ +using System.Collections; +using System.Collections.Generic; + +namespace System.Maui +{ + public interface IPicker : IText + { + string Title { get; } + Color TitleColor { get; } + Color TextColor { get; } + IList Items { get; } + IList ItemsSource { get; } + int SelectedIndex { get; set; } + object SelectedItem { get; } + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IProgress.cs b/Maui.Core/Views/IProgress.cs new file mode 100644 index 000000000000..67947f1b6e12 --- /dev/null +++ b/Maui.Core/Views/IProgress.cs @@ -0,0 +1,8 @@ +namespace System.Maui +{ + public interface IProgress : IView + { + double Progress { get; } + Color ProgressColor { get; } + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IPropertyMapperView.cs b/Maui.Core/Views/IPropertyMapperView.cs new file mode 100644 index 000000000000..85645d9d363c --- /dev/null +++ b/Maui.Core/Views/IPropertyMapperView.cs @@ -0,0 +1,5 @@ +namespace System.Maui { + public interface IPropertyMapperView { + PropertyMapper GetPropertyMapperOverrides (); + } +} diff --git a/Maui.Core/Views/IRange.cs b/Maui.Core/Views/IRange.cs new file mode 100644 index 000000000000..2e2924f33324 --- /dev/null +++ b/Maui.Core/Views/IRange.cs @@ -0,0 +1,9 @@ +namespace System.Maui +{ + public interface IRange : IView + { + double Minimum { get; } + double Maximum { get; } + double Value { get; set; } + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IScroll.cs b/Maui.Core/Views/IScroll.cs new file mode 100644 index 000000000000..a0e791644eee --- /dev/null +++ b/Maui.Core/Views/IScroll.cs @@ -0,0 +1,23 @@ +namespace System.Maui.Core +{ + public interface IScroll + { + ScrollOrientation Orientation { get; } + Size ContentSize { get; } + IView Content { get; } + double ScrollX { get; set; } + double ScrollY { get; set; } + bool HorizontalScrollBarVisible { get; } + bool VerticalScrollBarVisible { get; } + + void Scrolled(); + } + + public enum ScrollOrientation + { + Vertical, + Horizontal, + Both, + Neither + } +} diff --git a/Maui.Core/Views/ISearch.cs b/Maui.Core/Views/ISearch.cs new file mode 100644 index 000000000000..3d5270705b77 --- /dev/null +++ b/Maui.Core/Views/ISearch.cs @@ -0,0 +1,10 @@ + +namespace System.Maui +{ + public interface ISearch : ITextInput + { + void Search(); + void Cancel(); + Color CancelColor { get; } + } +} diff --git a/Maui.Core/Views/ISlider.cs b/Maui.Core/Views/ISlider.cs new file mode 100644 index 000000000000..2b817e277e5c --- /dev/null +++ b/Maui.Core/Views/ISlider.cs @@ -0,0 +1,16 @@ +namespace System.Maui +{ + public interface ISlider : IView + { + double Minimum { get; } + double Maximum { get; } + double Value { get; set; } + + Color MinimumTrackColor { get; } + Color MaximumTrackColor { get; } + Color ThumbColor { get; } + + void DragStarted(); + void DragCompleted(); + } +} \ No newline at end of file diff --git a/Maui.Core/Views/IStepper.cs b/Maui.Core/Views/IStepper.cs new file mode 100644 index 000000000000..cc38dedb404a --- /dev/null +++ b/Maui.Core/Views/IStepper.cs @@ -0,0 +1,7 @@ +namespace System.Maui +{ + public interface IStepper : IRange + { + double Increment { get; } + } +} diff --git a/Maui.Core/Views/ISwitch.cs b/Maui.Core/Views/ISwitch.cs new file mode 100644 index 000000000000..82ef1aef55c0 --- /dev/null +++ b/Maui.Core/Views/ISwitch.cs @@ -0,0 +1,9 @@ +namespace System.Maui +{ + public interface ISwitch : IView + { + bool IsOn { get; set; } + Color OnColor { get; } + Color ThumbColor { get; } + } +} diff --git a/Maui.Core/Views/ITabBar.cs b/Maui.Core/Views/ITabBar.cs new file mode 100644 index 000000000000..e36e1096a69c --- /dev/null +++ b/Maui.Core/Views/ITabBar.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Maui +{ + public interface ITabBar : IFrameworkElement + { + IList Children { get; } + } + + public interface ITab : IFrameworkElement + { + IFrameworkElement Content { get; } + } +} diff --git a/Maui.Core/Views/IText.cs b/Maui.Core/Views/IText.cs new file mode 100644 index 000000000000..a4318a1efd8e --- /dev/null +++ b/Maui.Core/Views/IText.cs @@ -0,0 +1,11 @@ +namespace System.Maui +{ + public interface IText : IView + { + string Text { get; } + + TextType TextType { get; } + //TODO: Add fonts and Colors + Color Color { get; } + } +} diff --git a/Maui.Core/Views/ITextInput.cs b/Maui.Core/Views/ITextInput.cs new file mode 100644 index 000000000000..6ed89ead575e --- /dev/null +++ b/Maui.Core/Views/ITextInput.cs @@ -0,0 +1,11 @@ +namespace System.Maui +{ + public interface ITextInput : IText + { + int MaxLength { get; } + string Placeholder { get; } + Color PlaceholderColor { get; } + new string Text { get; set; } + string IText.Text => Text; + } +} \ No newline at end of file diff --git a/Maui.Core/Views/ITimePicker.cs b/Maui.Core/Views/ITimePicker.cs new file mode 100644 index 000000000000..1956f28ce424 --- /dev/null +++ b/Maui.Core/Views/ITimePicker.cs @@ -0,0 +1,14 @@ +namespace System.Maui +{ + public interface ITimePicker : IText + { + TimeSpan SelectedTime { get; set; } + string ClockIdentifier { get; } + } + + public static class ClockIdentifiers + { + public const string TwelveHour = "12HourClock"; + public const string TwentyFourHour = "24HourClock"; + } +} diff --git a/Maui.Core/Views/IView.cs b/Maui.Core/Views/IView.cs new file mode 100644 index 000000000000..a8b7402e260e --- /dev/null +++ b/Maui.Core/Views/IView.cs @@ -0,0 +1,9 @@ +namespace System.Maui +{ + public interface IView : IFrameworkElement + { + + Alignment GetVerticalAlignment(ILayout layout) => Alignment.Fill; + Alignment GetHorizontalAlignment(ILayout layout) => Alignment.Fill; + } +} diff --git a/Maui.Core/Views/IWebView.cs b/Maui.Core/Views/IWebView.cs new file mode 100644 index 000000000000..c195e3c8f66b --- /dev/null +++ b/Maui.Core/Views/IWebView.cs @@ -0,0 +1,18 @@ +namespace System.Maui +{ + public interface IWebView : IView + { + bool CanGoBack { get; set; } + bool CanGoForward { get; set; } + WebViewSource Source { get; set; } + + event EventHandler EvalRequested; + event EvaluateJavaScriptDelegate EvaluateJavaScriptRequested; + event EventHandler GoBackRequested; + event EventHandler GoForwardRequested; + event EventHandler ReloadRequested; + + void Navigated(WebNavigatedEventArgs args); + void Navigating(WebNavigatingEventArgs args); + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/ColorTypeConverter.cs b/Maui.Core/Xaml/ColorTypeConverter.cs new file mode 100644 index 000000000000..48fa774cf998 --- /dev/null +++ b/Maui.Core/Xaml/ColorTypeConverter.cs @@ -0,0 +1,262 @@ +using System; +using System.Linq; +using System.Globalization; +using System.Maui.Extensions; + +namespace System.Maui.Xaml +{ + [ProvideCompiled("System.Maui.XamlC.ColorTypeConverter")] + [TypeConversion(typeof(Color))] + public class ColorTypeConverter : TypeConverter + { + // Supported inputs + // HEX #rgb, #argb, #rrggbb, #aarrggbb + // RGB rgb(255,0,0), rgb(100%,0%,0%) values in range 0-255 or 0%-100% + // RGBA rgba(255, 0, 0, 0.8), rgba(100%, 0%, 0%, 0.8) opacity is 0.0-1.0 + // HSL hsl(120, 100%, 50%) h is 0-360, s and l are 0%-100% + // HSLA hsla(120, 100%, 50%, .8) opacity is 0.0-1.0 + // Predefined color case insensitive + public override object ConvertFromInvariantString(string value) + { + if (value != null) + { + value = value.Trim(); + if (value.StartsWith("#", StringComparison.Ordinal)) + return Color.FromHex(value); + + if (value.StartsWith("rgba", StringComparison.OrdinalIgnoreCase)) { + var op = value.IndexOf('('); + var cp = value.LastIndexOf(')'); + if (op < 0 || cp < 0 || cp < op) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var quad = value.Substring(op + 1, cp - op - 1).Split(','); + if (quad.Length != 4) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var r = ParseColorValue(quad[0], 255, acceptPercent: true); + var g = ParseColorValue(quad[1], 255, acceptPercent: true); + var b = ParseColorValue(quad[2], 255, acceptPercent: true); + var a = ParseOpacity(quad[3]); + return new Color(r, g, b, a); + } + + if (value.StartsWith("rgb", StringComparison.OrdinalIgnoreCase)) { + var op = value.IndexOf('('); + var cp = value.LastIndexOf(')'); + if (op < 0 || cp < 0 || cp < op) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var triplet = value.Substring(op + 1, cp - op - 1).Split(','); + if (triplet.Length != 3) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var r = ParseColorValue(triplet[0], 255, acceptPercent: true); + var g = ParseColorValue(triplet[1], 255, acceptPercent: true); + var b = ParseColorValue(triplet[2], 255, acceptPercent: true); + return new Color(r, g, b); + } + + if (value.StartsWith("hsla", StringComparison.OrdinalIgnoreCase)) { + var op = value.IndexOf('('); + var cp = value.LastIndexOf(')'); + if (op < 0 || cp < 0 || cp < op) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var quad = value.Substring(op + 1, cp - op - 1).Split(','); + if (quad.Length != 4) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var h = ParseColorValue(quad[0], 360, acceptPercent: false); + var s = ParseColorValue(quad[1], 100, acceptPercent: true); + var l = ParseColorValue(quad[2], 100, acceptPercent: true); + var a = ParseOpacity(quad[3]); + return Color.FromHsla(h, s, l, a); + } + + if (value.StartsWith("hsl", StringComparison.OrdinalIgnoreCase)) { + var op = value.IndexOf('('); + var cp = value.LastIndexOf(')'); + if (op < 0 || cp < 0 || cp < op) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var triplet = value.Substring(op + 1, cp - op - 1).Split(','); + if (triplet.Length != 3) + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + var h = ParseColorValue(triplet[0], 360, acceptPercent: false); + var s = ParseColorValue(triplet[1], 100, acceptPercent: true); + var l = ParseColorValue(triplet[2], 100, acceptPercent: true); + return Color.FromHsla(h, s, l); + } + + string[] parts = value.Split('.'); + if (parts.Length == 1 || (parts.Length == 2 && parts[0] == "Color")) + { + string color = parts[parts.Length - 1]; + switch (color.ToLowerInvariant()) { + case "default": return Color.Default; + case "accent": return Color.Accent; + case "aliceblue": return Color.AliceBlue; + case "antiquewhite": return Color.AntiqueWhite; + case "aqua": return Color.Aqua; + case "aquamarine": return Color.Aquamarine; + case "azure": return Color.Azure; + case "beige": return Color.Beige; + case "bisque": return Color.Bisque; + case "black": return Color.Black; + case "blanchedalmond": return Color.BlanchedAlmond; + case "blue": return Color.Blue; + case "blueViolet": return Color.BlueViolet; + case "brown": return Color.Brown; + case "burlywood": return Color.BurlyWood; + case "cadetblue": return Color.CadetBlue; + case "chartreuse": return Color.Chartreuse; + case "chocolate": return Color.Chocolate; + case "coral": return Color.Coral; + case "cornflowerblue": return Color.CornflowerBlue; + case "cornsilk": return Color.Cornsilk; + case "crimson": return Color.Crimson; + case "cyan": return Color.Cyan; + case "darkblue": return Color.DarkBlue; + case "darkcyan": return Color.DarkCyan; + case "darkgoldenrod": return Color.DarkGoldenrod; + case "darkgray": return Color.DarkGray; + case "darkgreen": return Color.DarkGreen; + case "darkkhaki": return Color.DarkKhaki; + case "darkmagenta": return Color.DarkMagenta; + case "darkolivegreen": return Color.DarkOliveGreen; + case "darkorange": return Color.DarkOrange; + case "darkorchid": return Color.DarkOrchid; + case "darkred": return Color.DarkRed; + case "darksalmon": return Color.DarkSalmon; + case "darkseagreen": return Color.DarkSeaGreen; + case "darkslateblue": return Color.DarkSlateBlue; + case "darkslategray": return Color.DarkSlateGray; + case "darkturquoise": return Color.DarkTurquoise; + case "darkviolet": return Color.DarkViolet; + case "deeppink": return Color.DeepPink; + case "deepskyblue": return Color.DeepSkyBlue; + case "dimgray": return Color.DimGray; + case "dodgerblue": return Color.DodgerBlue; + case "firebrick": return Color.Firebrick; + case "floralwhite": return Color.FloralWhite; + case "forestgreen": return Color.ForestGreen; + case "fuchsia": return Color.Fuchsia; + case "gainsboro": return Color.Gainsboro; + case "ghostwhite": return Color.GhostWhite; + case "gold": return Color.Gold; + case "goldenrod": return Color.Goldenrod; + case "gray": return Color.Gray; + case "green": return Color.Green; + case "greenyellow": return Color.GreenYellow; + case "honeydew": return Color.Honeydew; + case "hotpink": return Color.HotPink; + case "indianred": return Color.IndianRed; + case "indigo": return Color.Indigo; + case "ivory": return Color.Ivory; + case "khaki": return Color.Khaki; + case "lavender": return Color.Lavender; + case "lavenderblush": return Color.LavenderBlush; + case "lawngreen": return Color.LawnGreen; + case "lemonchiffon": return Color.LemonChiffon; + case "lightblue": return Color.LightBlue; + case "lightcoral": return Color.LightCoral; + case "lightcyan": return Color.LightCyan; + case "lightgoldenrodyellow": return Color.LightGoldenrodYellow; + case "lightgrey": + case "lightgray": return Color.LightGray; + case "lightgreen": return Color.LightGreen; + case "lightpink": return Color.LightPink; + case "lightsalmon": return Color.LightSalmon; + case "lightseagreen": return Color.LightSeaGreen; + case "lightskyblue": return Color.LightSkyBlue; + case "lightslategray": return Color.LightSlateGray; + case "lightsteelblue": return Color.LightSteelBlue; + case "lightyellow": return Color.LightYellow; + case "lime": return Color.Lime; + case "limegreen": return Color.LimeGreen; + case "linen": return Color.Linen; + case "magenta": return Color.Magenta; + case "maroon": return Color.Maroon; + case "mediumaquamarine": return Color.MediumAquamarine; + case "mediumblue": return Color.MediumBlue; + case "mediumorchid": return Color.MediumOrchid; + case "mediumpurple": return Color.MediumPurple; + case "mediumseagreen": return Color.MediumSeaGreen; + case "mediumslateblue": return Color.MediumSlateBlue; + case "mediumspringgreen": return Color.MediumSpringGreen; + case "mediumturquoise": return Color.MediumTurquoise; + case "mediumvioletred": return Color.MediumVioletRed; + case "midnightblue": return Color.MidnightBlue; + case "mintcream": return Color.MintCream; + case "mistyrose": return Color.MistyRose; + case "moccasin": return Color.Moccasin; + case "navajowhite": return Color.NavajoWhite; + case "navy": return Color.Navy; + case "oldlace": return Color.OldLace; + case "olive": return Color.Olive; + case "olivedrab": return Color.OliveDrab; + case "orange": return Color.Orange; + case "orangered": return Color.OrangeRed; + case "orchid": return Color.Orchid; + case "palegoldenrod": return Color.PaleGoldenrod; + case "palegreen": return Color.PaleGreen; + case "paleturquoise": return Color.PaleTurquoise; + case "palevioletred": return Color.PaleVioletRed; + case "papayawhip": return Color.PapayaWhip; + case "peachpuff": return Color.PeachPuff; + case "peru": return Color.Peru; + case "pink": return Color.Pink; + case "plum": return Color.Plum; + case "powderblue": return Color.PowderBlue; + case "purple": return Color.Purple; + case "red": return Color.Red; + case "rosybrown": return Color.RosyBrown; + case "royalblue": return Color.RoyalBlue; + case "saddlebrown": return Color.SaddleBrown; + case "salmon": return Color.Salmon; + case "sandybrown": return Color.SandyBrown; + case "seagreen": return Color.SeaGreen; + case "seashell": return Color.SeaShell; + case "sienna": return Color.Sienna; + case "silver": return Color.Silver; + case "skyblue": return Color.SkyBlue; + case "slateblue": return Color.SlateBlue; + case "slategray": return Color.SlateGray; + case "snow": return Color.Snow; + case "springgreen": return Color.SpringGreen; + case "steelblue": return Color.SteelBlue; + case "tan": return Color.Tan; + case "teal": return Color.Teal; + case "thistle": return Color.Thistle; + case "tomato": return Color.Tomato; + case "transparent": return Color.Transparent; + case "turquoise": return Color.Turquoise; + case "violet": return Color.Violet; + case "wheat": return Color.Wheat; + case "white": return Color.White; + case "whitesmoke": return Color.WhiteSmoke; + case "yellow": return Color.Yellow; + case "yellowgreen": return Color.YellowGreen; + } + var field = typeof(Color).GetFields().FirstOrDefault(fi => fi.IsStatic && string.Equals(fi.Name, color, StringComparison.OrdinalIgnoreCase)); + if (field != null) + return (Color)field.GetValue(null); + var property = typeof(Color).GetProperties().FirstOrDefault(pi => string.Equals(pi.Name, color, StringComparison.OrdinalIgnoreCase) && pi.CanRead && pi.GetMethod.IsStatic); + if (property != null) + return (Color)property.GetValue(null, null); + } + } + + throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(Color)}"); + } + + static double ParseColorValue(string elem, int maxValue, bool acceptPercent) + { + elem = elem.Trim(); + if (elem.EndsWith("%", StringComparison.Ordinal) && acceptPercent) { + maxValue = 100; + elem = elem.Substring(0, elem.Length - 1); + } + return double.Parse(elem, NumberStyles.Number, CultureInfo.InvariantCulture).Clamp(0, maxValue) / maxValue; + } + + static double ParseOpacity(string elem) + { + return double.Parse(elem, NumberStyles.Number, CultureInfo.InvariantCulture).Clamp(0, 1); + } + } +} diff --git a/Maui.Core/Xaml/PointTypeConverter.cs b/Maui.Core/Xaml/PointTypeConverter.cs new file mode 100644 index 000000000000..e0de11410a40 --- /dev/null +++ b/Maui.Core/Xaml/PointTypeConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace System.Maui.Xaml +{ + [TypeConversion(typeof(Point))] + public class PointTypeConverter : TypeConverter + { + public override object ConvertFromInvariantString(string value) + { + if (value != null) + { + double x, y; + string[] xy = value.Split(','); + if (xy.Length == 2 && double.TryParse(xy[0], NumberStyles.Number, CultureInfo.InvariantCulture, out x) && double.TryParse(xy[1], NumberStyles.Number, CultureInfo.InvariantCulture, out y)) + return new Point(x, y); + } + + throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", value, typeof(Point))); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/ProvideCompiledAttribute.cs b/Maui.Core/Xaml/ProvideCompiledAttribute.cs new file mode 100644 index 000000000000..5c5ab1f0903d --- /dev/null +++ b/Maui.Core/Xaml/ProvideCompiledAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace System.Maui.Xaml +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class ProvideCompiledAttribute : Attribute + { + public string CompiledVersion { get; } + + public ProvideCompiledAttribute (string compiledVersion) + { + CompiledVersion = compiledVersion; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/RectangleTypeConverter.cs b/Maui.Core/Xaml/RectangleTypeConverter.cs new file mode 100644 index 000000000000..911b8c620e94 --- /dev/null +++ b/Maui.Core/Xaml/RectangleTypeConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; + +namespace System.Maui.Xaml +{ + [ProvideCompiled("System.Maui.XamlC.RectangleTypeConverter")] + [TypeConversion(typeof(Rectangle))] + public class RectangleTypeConverter : TypeConverter + { + public override object ConvertFromInvariantString(string value) + { + if (value != null) + { + string[] xywh = value.Split(','); + if ( xywh.Length == 4 + && double.TryParse(xywh[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double x) + && double.TryParse(xywh[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double y) + && double.TryParse(xywh[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double w) + && double.TryParse(xywh[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double h)) + return new Rectangle(x, y, w, h); + } + + throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", value, typeof(Rectangle))); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/SizeTypeConverter.cs b/Maui.Core/Xaml/SizeTypeConverter.cs new file mode 100644 index 000000000000..47986d332eb1 --- /dev/null +++ b/Maui.Core/Xaml/SizeTypeConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace System.Maui.Xaml +{ + [TypeConversion(typeof(Size))] + public class SizeTypeConverter : TypeConverter + { + public override object ConvertFromInvariantString(string value) + { + if (value != null) { + string[] wh = value.Split(','); + if (wh.Length == 2 + && double.TryParse(wh[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double w) + && double.TryParse(wh[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double h)) + return new Size(w, h); + } + + throw new InvalidOperationException(string.Format("Cannot convert \"{0}\" into {1}", value, typeof(Size))); + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/TypeConversionAttribute.cs b/Maui.Core/Xaml/TypeConversionAttribute.cs new file mode 100644 index 000000000000..fb3e3fc40c25 --- /dev/null +++ b/Maui.Core/Xaml/TypeConversionAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace System.Maui.Xaml +{ + [System.AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class TypeConversionAttribute : Attribute + { + public Type TargetType { get; private set; } + + public TypeConversionAttribute(Type targetType) + { + TargetType = targetType; + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/TypeConverter.cs b/Maui.Core/Xaml/TypeConverter.cs new file mode 100644 index 000000000000..c85aa881068a --- /dev/null +++ b/Maui.Core/Xaml/TypeConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace System.Maui +{ + public abstract class TypeConverter + { + public virtual bool CanConvertFrom(Type sourceType) + { + if (sourceType == null) + throw new ArgumentNullException(nameof(sourceType)); + + return sourceType == typeof(string); + } + + [Obsolete("ConvertFrom is obsolete as of version 2.2.0. Please use ConvertFromInvariantString (string) instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual object ConvertFrom(object o) + { + return null; + } + + [Obsolete("ConvertFrom is obsolete as of version 2.2.0. Please use ConvertFromInvariantString (string) instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual object ConvertFrom(CultureInfo culture, object o) + { + return null; + } + + public virtual object ConvertFromInvariantString(string value) + { +#pragma warning disable 0618 // retain until ConvertFrom removed + return ConvertFrom(CultureInfo.InvariantCulture, value); +#pragma warning restore + } + } +} \ No newline at end of file diff --git a/Maui.Core/Xaml/TypeConverterAttribute.cs b/Maui.Core/Xaml/TypeConverterAttribute.cs new file mode 100644 index 000000000000..1eb355257b82 --- /dev/null +++ b/Maui.Core/Xaml/TypeConverterAttribute.cs @@ -0,0 +1,74 @@ +// +// System.ComponentModel.TypeConverterAttribute +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Andreas Nahr (ClassDevelopment@A-SoftTech.com) +// +// (C) 2002 Ximian, Inc (http://www.ximian.com) +// (C) 2003 Andreas Nahr +// + +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace System.Maui.Xaml +{ + [AttributeUsage(AttributeTargets.All)] + public sealed class TypeConverterAttribute : Attribute + { + public static string[] TypeConvertersType = { "System.Maui.Core.TypeConverterAttribute", "System.ComponentModel.TypeConverterAttribute" }; + + public static readonly TypeConverterAttribute Default = new TypeConverterAttribute(); + + public TypeConverterAttribute() + { + ConverterTypeName = ""; + } + + public TypeConverterAttribute(string typeName) + { + ConverterTypeName = typeName; + } + + public TypeConverterAttribute(Type type) + { + ConverterTypeName = type.AssemblyQualifiedName; + } + + public string ConverterTypeName { get; } + + public override bool Equals(object obj) + { + if (!(obj is TypeConverterAttribute)) + return false; + + return ((TypeConverterAttribute)obj).ConverterTypeName == ConverterTypeName; + } + + public override int GetHashCode() + { + return ConverterTypeName.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Maui.Core/global.json b/Maui.Core/global.json new file mode 100644 index 000000000000..80d84a257c63 --- /dev/null +++ b/Maui.Core/global.json @@ -0,0 +1,5 @@ +{ + "msbuild-sdks": { + "MSBuild.Sdk.Extras": "2.0.54" + } +} \ No newline at end of file diff --git a/System.Maui.Core/System.Maui.Core.csproj b/System.Maui.Core/System.Maui.Core.csproj index b77d3b5e0e38..4ba4a135f305 100644 --- a/System.Maui.Core/System.Maui.Core.csproj +++ b/System.Maui.Core/System.Maui.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard1.0 diff --git a/System.Maui.sln b/System.Maui.sln index 3ea4979811c7..e683dc5f81a7 100644 --- a/System.Maui.sln +++ b/System.Maui.sln @@ -158,6 +158,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Maui.Platform.UAP.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Maui.Platform.iOS.UnitTests", "System.Maui.Platform.iOS.UnitTests\System.Maui.Platform.iOS.UnitTests.csproj", "{52C50E88-7F15-45FE-A63C-8E7C76CA2BAE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Maui.Core", "Maui.Core\Maui.Core.csproj", "{EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution System.Maui.Controls.Issues\System.Maui.Controls.Issues.Shared\System.Maui.Controls.Issues.Shared.projitems*{0a39a74b-6f7a-4d41-84f2-b0ccdce899df}*SharedItemsImports = 4 @@ -1531,6 +1533,34 @@ Global {52C50E88-7F15-45FE-A63C-8E7C76CA2BAE}.Release|x64.Build.0 = Release|Any CPU {52C50E88-7F15-45FE-A63C-8E7C76CA2BAE}.Release|x86.ActiveCfg = Release|Any CPU {52C50E88-7F15-45FE-A63C-8E7C76CA2BAE}.Release|x86.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|ARM.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|ARM64.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|x64.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Debug|x86.Build.0 = Debug|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|Any CPU.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|ARM.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|ARM.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|ARM64.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|ARM64.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|iPhone.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|x64.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|x64.Build.0 = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|x86.ActiveCfg = Release|Any CPU + {EFAA0E1A-1DA0-491C-A55D-019A35E8B0FC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE