diff --git a/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml b/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml index 2e4154a..5e913ce 100644 --- a/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml +++ b/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml @@ -67,5 +67,12 @@ X="100" Y="200" Data="M 10,100 L 100,100 100,50Z" Stroke="Black" /> + diff --git a/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml.cs b/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml.cs index 3223748..0f201cf 100644 --- a/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml.cs +++ b/src/AlohaKit.UI.Gallery/Views/XAML/MainPage.xaml.cs @@ -9,7 +9,12 @@ void OnEllipseTapped(object sender, EventArgs e) { - DisplayAlert("Gestures", "Ellipse tapped.", "Ok"); + DisplayAlert("Gestures", "Ellipse Tapped.", "Ok"); + } + + void OnButtonClicked(object sender, EventArgs e) + { + DisplayAlert("Button", "Button Clicked.", "Ok"); } } } \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/Button.cs b/src/AlohaKit.UI/Controls/Button.cs new file mode 100644 index 0000000..036f9fd --- /dev/null +++ b/src/AlohaKit.UI/Controls/Button.cs @@ -0,0 +1,71 @@ +using Microsoft.Maui; +using Microsoft.Maui.Graphics.Text; + +namespace AlohaKit.UI +{ + public abstract class ButtonBase : View + { + public static readonly BindableProperty TextProperty = + BindableProperty.Create(nameof(Text), typeof(string), typeof(ButtonBase), string.Empty, + propertyChanged: InvalidatePropertyChanged); + + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(ButtonBase), null, + propertyChanged: InvalidatePropertyChanged); + + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(double), typeof(ButtonBase), 14.0d, + propertyChanged: InvalidatePropertyChanged); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public Color TextColor + { + get => (Color)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + public double FontSize + { + get => (double)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + public event EventHandler Clicked; + public event EventHandler Pressed; + public event EventHandler Released; + + public override void StartInteraction(PointF[] points) + { + base.StartInteraction(points); + + Pressed?.Invoke(this, EventArgs.Empty); + Clicked?.Invoke(this, EventArgs.Empty); + } + + public override void EndInteraction(PointF[] points, bool isInsideBounds) + { + base.EndInteraction(points, isInsideBounds); + + Released?.Invoke(this, EventArgs.Empty); + } + } + + public class Button : +#if ANDROID + Material.Button +#elif IOS || MACCATALYST + Cupertino.Button +#elif WINDOWS + Fluent.Button +#else + Material.Button +#endif + { + + } +} \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/CanvasView.cs b/src/AlohaKit.UI/Controls/CanvasView.cs index 8065129..2fcb930 100644 --- a/src/AlohaKit.UI/Controls/CanvasView.cs +++ b/src/AlohaKit.UI/Controls/CanvasView.cs @@ -21,19 +21,27 @@ namespace AlohaKit.UI } } + public interface ICanvasView + { + void Draw(ICanvas canvas, RectF bounds); + void Invalidate(); + } + [ContentProperty(nameof(Children))] - public class CanvasView : GraphicsView + public class CanvasView : GraphicsView, ICanvasView, IDisposable { public CanvasView() { - Children = new ElementsCollection(); + Children = new ElementsCollection(this); Drawable = new CanvasViewDrawable(this); StartInteraction += OnCanvasViewStartInteraction; - } + EndInteraction += OnCanvasViewEndInteraction; + CancelInteraction += OnCanvasViewCancelInteraction; + } - public ElementsCollection Children { get; internal set; } + public ElementsCollection Children { get; internal set; } internal void DrawCore(ICanvas canvas, RectF bounds) { @@ -51,14 +59,23 @@ namespace AlohaKit.UI } } + void IDisposable.Dispose() + { + StartInteraction -= OnCanvasViewStartInteraction; + EndInteraction -= OnCanvasViewEndInteraction; + CancelInteraction -= OnCanvasViewCancelInteraction; + } + void OnCanvasViewStartInteraction(object sender, TouchEventArgs e) { var touchPoint = e.Touches[0]; foreach (var child in Children) { - if (child.IsVisible && child is View view && view.TouchInside(touchPoint)) + if (child.IsVisible && child is View view && view.IsInsideBounds(touchPoint)) { + view.StartInteraction(e.Touches); + foreach (var gesture in view.GestureRecognizers) { if (gesture is TapGestureRecognizer tapGestureRecognizer) @@ -67,5 +84,29 @@ namespace AlohaKit.UI } } } - } + + void OnCanvasViewEndInteraction(object sender, TouchEventArgs e) + { + var touchPoint = e.Touches[0]; + + foreach (var child in Children) + { + if (child.IsVisible && child is View view && view.IsInsideBounds(touchPoint)) + { + view.EndInteraction(e.Touches, e.IsInsideBounds); + } + } + } + + void OnCanvasViewCancelInteraction(object sender, EventArgs e) + { + foreach (var child in Children) + { + if (child.IsVisible && child is View view) + { + view.CancelInteraction(); + } + } + } + } } \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/Cupertino/Button.cs b/src/AlohaKit.UI/Controls/Cupertino/Button.cs new file mode 100644 index 0000000..6c515ab --- /dev/null +++ b/src/AlohaKit.UI/Controls/Cupertino/Button.cs @@ -0,0 +1,62 @@ +namespace AlohaKit.UI.Cupertino +{ + public class Button : ButtonBase + { + const string BackgroundColor = "#007AFF"; + const float CornerRadius = 2.0f; + const float MinimumHeight = 44f; + + public override void Draw(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + base.Draw(canvas, bounds); + + DrawBackground(canvas, bounds); + DrawText(canvas, bounds); + + canvas.RestoreState(); + } + + public virtual void DrawBackground(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + if (Background is SolidColorBrush solidColorBrush) + { + if (solidColorBrush.Color != null) + canvas.FillColor = solidColorBrush.Color; + else + canvas.FillColor = Color.FromArgb(BackgroundColor); + } + else + canvas.SetFillPaint(Background, bounds); + + float height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + canvas.FillRoundedRectangle(X, Y, WidthRequest, height, CornerRadius); + + canvas.RestoreState(); + } + + public virtual void DrawText(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + canvas.FontColor = TextColor; + canvas.FontSize = (float)FontSize; + + float height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + canvas.DrawString(Text, X, Y, WidthRequest, height, HorizontalAlignment.Center, VerticalAlignment.Center); + + canvas.RestoreState(); + } + } +} \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/Element.cs b/src/AlohaKit.UI/Controls/Element.cs index cd9ea4a..8ca45e2 100644 --- a/src/AlohaKit.UI/Controls/Element.cs +++ b/src/AlohaKit.UI/Controls/Element.cs @@ -12,8 +12,6 @@ public class Element : VisualElement, IElement { IElement _parent; - RectF _childrenBounds; - public Element() { diff --git a/src/AlohaKit.UI/Controls/ElementsCollection.cs b/src/AlohaKit.UI/Controls/ElementsCollection.cs index 54a5509..a68c9cd 100644 --- a/src/AlohaKit.UI/Controls/ElementsCollection.cs +++ b/src/AlohaKit.UI/Controls/ElementsCollection.cs @@ -6,6 +6,7 @@ namespace AlohaKit.UI public class ElementsCollection : ObservableCollection { readonly IElement _parent; + readonly ICanvasView _canvasView; public ElementsCollection() { @@ -15,9 +16,14 @@ namespace AlohaKit.UI internal ElementsCollection(IElement parent) { _parent = parent; - } + } - protected override void ClearItems() + internal ElementsCollection(ICanvasView canvasView) + { + _canvasView = canvasView; + } + + protected override void ClearItems() { base.ClearItems(); } @@ -47,6 +53,7 @@ namespace AlohaKit.UI void Invalidate() { + _canvasView?.Invalidate(); _parent?.Invalidate(); } } diff --git a/src/AlohaKit.UI/Controls/Fluent/Button.cs b/src/AlohaKit.UI/Controls/Fluent/Button.cs new file mode 100644 index 0000000..018b1dc --- /dev/null +++ b/src/AlohaKit.UI/Controls/Fluent/Button.cs @@ -0,0 +1,99 @@ +using AlohaKit.UI.Extensions; + +namespace AlohaKit.UI.Fluent +{ + public class Button : ButtonBase + { + const string BackgroundColor = "#2A2A2A"; + const float CornerRadius = 4.0f; + const float MinimumHeight = 32f; + + public override void Draw(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + base.Draw(canvas, bounds); + + DrawBackground(canvas, bounds); + DrawText(canvas, bounds); + + canvas.RestoreState(); + } + + public virtual void DrawBackground(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + var x = X; + var y = Y; + + var width = WidthRequest; + var height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + var backgroundColor = Color.FromArgb(BackgroundColor); + + if (Background is SolidColorBrush backgroundBrush) + { + if (backgroundBrush.Color != null) + backgroundColor = backgroundBrush.Color; + } + + var border = new LinearGradientPaint + { + GradientStops = new PaintGradientStop[] + { + new PaintGradientStop(0.0f, backgroundColor.Lighter()), + new PaintGradientStop(0.9f, backgroundColor.Darker()) + }, + StartPoint = new Point(0, 0), + EndPoint = new Point(0, 1) + }; + + canvas.SetFillPaint(border, bounds); + + canvas.FillRoundedRectangle(x, y, width, height, CornerRadius); + + canvas.RestoreState(); + + canvas.SaveState(); + + canvas.StrokeColor = Colors.Black; + + if (Background is SolidColorBrush borderBrush) + { + if (borderBrush.Color != null) + canvas.FillColor = borderBrush.Color; + else + canvas.FillColor = backgroundColor; + } + else + canvas.SetFillPaint(Background, bounds); + + var strokeWidth = 1; + float margin = strokeWidth * 2; + canvas.FillRoundedRectangle(x + strokeWidth, y + strokeWidth, width - margin, height - margin, CornerRadius); + + canvas.RestoreState(); + } + + public virtual void DrawText(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + canvas.FontColor = TextColor; + canvas.FontSize = (float)FontSize; + + float height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + canvas.DrawString(Text, X, Y, WidthRequest, height, HorizontalAlignment.Center, VerticalAlignment.Center); + + canvas.RestoreState(); + } + } +} \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/Material/Button.cs b/src/AlohaKit.UI/Controls/Material/Button.cs new file mode 100644 index 0000000..eedd4b7 --- /dev/null +++ b/src/AlohaKit.UI/Controls/Material/Button.cs @@ -0,0 +1,62 @@ +namespace AlohaKit.UI.Material +{ + public class Button : ButtonBase + { + const string BackgroundColor = "#2196f3"; + const float MinimumHeight = 36f; + const float CornerRadius = 2.0f; + + public override void Draw(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + base.Draw(canvas, bounds); + + DrawBackground(canvas, bounds); + DrawText(canvas, bounds); + + canvas.RestoreState(); + } + + public void DrawBackground(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + if (Background is SolidColorBrush solidColorBrush) + { + if (solidColorBrush.Color != null) + canvas.FillColor = solidColorBrush.Color; + else + canvas.FillColor = Color.FromArgb(BackgroundColor); + } + else + canvas.SetFillPaint(Background, bounds); + + float height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + canvas.FillRoundedRectangle(X, Y, WidthRequest, height, CornerRadius); + + canvas.RestoreState(); + } + + public void DrawText(ICanvas canvas, RectF bounds) + { + canvas.SaveState(); + + canvas.FontColor = TextColor; + canvas.FontSize = (float)FontSize; + + float height = MinimumHeight; + + if (!float.IsNaN(HeightRequest)) + height = HeightRequest; + + canvas.DrawString(Text.ToUpper(), X, Y, WidthRequest, height, HorizontalAlignment.Center, VerticalAlignment.Center); + + canvas.RestoreState(); + } + } +} \ No newline at end of file diff --git a/src/AlohaKit.UI/Controls/View.cs b/src/AlohaKit.UI/Controls/View.cs index a7542b1..552b91f 100644 --- a/src/AlohaKit.UI/Controls/View.cs +++ b/src/AlohaKit.UI/Controls/View.cs @@ -9,7 +9,15 @@ namespace AlohaKit.UI public interface IView : IElement { Brush Background { get; set; } - } + + void StartHoverInteraction(PointF[] points); + void MoveHoverInteraction(PointF[] points); + void EndHoverInteraction(); + void StartInteraction(PointF[] points); + void DragInteraction(PointF[] points); + void EndInteraction(PointF[] points, bool isInsideBounds); + void CancelInteraction(); + } public class View : Element, IView { @@ -58,7 +66,8 @@ namespace AlohaKit.UI } public static readonly BindableProperty BackgroundProperty = - BindableProperty.Create(nameof(Background), typeof(Brush), typeof(View), null, propertyChanged: BackgroundPropertyChanged); + BindableProperty.Create(nameof(Background), typeof(Brush), typeof(View), null, + propertyChanged: BackgroundPropertyChanged); public static void BackgroundPropertyChanged(BindableObject bindableObject, object oldValue, object newValue) { @@ -93,7 +102,21 @@ namespace AlohaKit.UI } } - void DrawBackground(ICanvas canvas, RectF bounds) + public virtual void CancelInteraction() { } + + public virtual void DragInteraction(PointF[] points) { } + + public virtual void EndHoverInteraction() { } + + public virtual void EndInteraction(PointF[] points, bool isInsideBounds) { } + + public virtual void StartHoverInteraction(PointF[] points) { } + + public virtual void MoveHoverInteraction(PointF[] points) { } + + public virtual void StartInteraction(PointF[] points) { } + + void DrawBackground(ICanvas canvas, RectF bounds) { if (Background is SolidColorBrush solidColorBrush) canvas.FillColor = solidColorBrush.Color; diff --git a/src/AlohaKit.UI/Extensions/ColorExtensions.cs b/src/AlohaKit.UI/Extensions/ColorExtensions.cs new file mode 100644 index 0000000..d94d829 --- /dev/null +++ b/src/AlohaKit.UI/Extensions/ColorExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace AlohaKit.UI.Extensions +{ + public static class ColorExtensions + { + const float LighterFactor = 1.1f; + const float DarkerFactor = 0.9f; + + public static Color Lighter(this Color color) + { + return new Color( + color.Red * LighterFactor, + color.Green * LighterFactor, + color.Blue * LighterFactor, + color.Alpha); + } + + public static Color Darker(this Color color) + { + return new Color( + color.Red * DarkerFactor, + color.Green * DarkerFactor, + color.Blue * DarkerFactor, + color.Alpha); + } + } +} \ No newline at end of file diff --git a/src/AlohaKit.UI/Extensions/GestureExtensions.cs b/src/AlohaKit.UI/Extensions/GestureExtensions.cs index 32d852b..7da2894 100644 --- a/src/AlohaKit.UI/Extensions/GestureExtensions.cs +++ b/src/AlohaKit.UI/Extensions/GestureExtensions.cs @@ -2,9 +2,22 @@ { public static class GestureExtensions { - public static bool TouchInside(this T view, PointF touchPoint) where T : View + public static bool IsInsideBounds(this T view, PointF touchPoint) where T : View { - var bounds = new RectF(view.X, view.Y, view.WidthRequest, view.HeightRequest); + if (view == null) + return false; + + var minimumTouchSize = 24f; + + var width = view.WidthRequest; + if (float.IsNaN(width)) + width = minimumTouchSize; + + var height = view.HeightRequest; + if (float.IsNaN(height)) + height = minimumTouchSize; + + var bounds = new RectF(view.X, view.Y, width, height); if (bounds.Contains(touchPoint)) return true;