diff --git a/ReCap.CommonUI/.gitignore b/ReCap.CommonUI/.gitignore new file mode 100644 index 0000000..913207c --- /dev/null +++ b/ReCap.CommonUI/.gitignore @@ -0,0 +1,44 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode/ + +# Rider +.idea/ + +# Visual Studio +.vs/ + +# Fleet +.fleet/ + +# Code Rush +.cr/ + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Pp]ublish/ +msbuild.log +msbuild.err +msbuild.wrn \ No newline at end of file diff --git a/ReCap.CommonUI/Attached/BindDynamicResource.cs b/ReCap.CommonUI/Attached/BindDynamicResource.cs new file mode 100755 index 0000000..4bf4a64 --- /dev/null +++ b/ReCap.CommonUI/Attached/BindDynamicResource.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml.MarkupExtensions; + +using AvPropChangedArgs = Avalonia.AvaloniaPropertyChangedEventArgs; + +namespace ReCap.CommonUI +{ + public class BindDynamicResource + : AvaloniaObject + { + public static readonly AttachedProperty ResourceKeyProperty = + AvaloniaProperty.RegisterAttached("ResourceKey", null); + public static object GetResourceKey(Control control) + => control.GetValue(ResourceKeyProperty); + public static void SetResourceKey(Control control, double value) + => control.SetValue(ResourceKeyProperty, value); + + + static BindDynamicResource() + { + ResourceKeyProperty.Changed.AddClassHandler(ContentControl_ResourceKeyProperty_Changed); + ResourceKeyProperty.Changed.AddClassHandler(TemplatedControl_ResourceKeyProperty_Changed); + ResourceKeyProperty.Changed.AddClassHandler(TextBlock_ResourceKeyProperty_Changed); + } + + static void ContentControl_ResourceKeyProperty_Changed(ContentControl control, AvPropChangedArgs args) + => ResourceKeyProperty_Changed(control, args, ContentControl.ContentProperty); + static void TemplatedControl_ResourceKeyProperty_Changed(TemplatedControl control, AvPropChangedArgs args) + => ResourceKeyProperty_Changed(control, args, TemplatedControl.TemplateProperty); + static void TextBlock_ResourceKeyProperty_Changed(TextBlock control, AvPropChangedArgs args) + => ResourceKeyProperty_Changed(control, args, TextBlock.TextProperty); + /* + { + GetOldAndNewKey(args, out object oldKey, out object newKey); + + if (oldKey == newKey) + return; + //control.Bind(TextBlock.TextProperty, new DynamicResourceExtension(newKey)) + } + */ + + static readonly Dictionary, IDisposable> _bindings = new(); + static void ResourceKeyProperty_Changed(TControl control, AvPropChangedArgs args, AvaloniaProperty prop) + where TControl : Control + { + GetOldAndNewKey(args, out object oldKey, out object newKey); + + if (oldKey == newKey) + return; + + //_bindings.Keys.FirstOrDefault(x => x.try) + if (TryGetBindingFor(control, out WeakReference key, out IDisposable prevBinding)) + { + _bindings.Remove(key); + prevBinding.Dispose(); + } + + if (newKey == null) + return; + + var newBinding = control.Bind(prop, new DynamicResourceExtension(newKey)); + _bindings.Add(new(control), newBinding); + } + + static bool TryGetBindingFor(Control control, out WeakReference key, out IDisposable binding) + { + var keys = (_bindings != null) + ? _bindings.Keys + : null + ; + + if (keys == null) + goto fail; + if (keys.Count <= 0) + goto fail; + + + for (int i = 0; i < _bindings.Keys.Count; i++) + { + var k = _bindings.Keys.ElementAt(i); + if (k.TryGetTarget(out Control target)) + { + if (target != control) + continue; + + key = k; + binding = _bindings[k]; + return true; + } + else + { + _bindings.Remove(k); + i--; + } + } + + fail: + key = default; + binding = default; + return false; + } + + static void GetOldAndNewKey(AvPropChangedArgs args, out object oldKey, out object newKey) + => (oldKey, newKey) = args.GetOldAndNewValue(); + } +} diff --git a/ReCap.CommonUI/Attached/ScrollBarInset.cs b/ReCap.CommonUI/Attached/ScrollBarInset.cs new file mode 100755 index 0000000..2b314b2 --- /dev/null +++ b/ReCap.CommonUI/Attached/ScrollBarInset.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace ReCap.CommonUI +{ + public class ScrollBarInset + : AvaloniaObject + { + public static readonly AttachedProperty VerticalProperty = + AvaloniaProperty.RegisterAttached("Vertical", 0.0); + public static double GetVertical(ScrollViewer control) + => control.GetValue(VerticalProperty); + public static void SetVertical(ScrollViewer control, double value) + => control.SetValue(VerticalProperty, value); + + + public static readonly AttachedProperty HorizontalProperty = + AvaloniaProperty.RegisterAttached("Horizontal", 0.0); + public static double GetHorizontal(ScrollViewer control) + => control.GetValue(HorizontalProperty); + public static void SetHorizontal(ScrollViewer control, double value) + => control.SetValue(HorizontalProperty, value); + } +} diff --git a/ReCap.CommonUI/Attached/TextFormatter.cs b/ReCap.CommonUI/Attached/TextFormatter.cs new file mode 100755 index 0000000..165262e --- /dev/null +++ b/ReCap.CommonUI/Attached/TextFormatter.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reactive; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Chrome; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Input; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Reactive; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace ReCap.CommonUI +{ + public class TextFormatter + : AvaloniaObject + { + public static readonly AttachedProperty FormatterProperty = + AvaloniaProperty.RegisterAttached("Formatter", null); + public static TextFormatter GetFormatter(TextBlock control) + => control.GetValue(FormatterProperty); + public static void SetFormatter(TextBlock control, double value) + => control.SetValue(FormatterProperty, value); + + + + /// + /// Defines the property. + /// + public static readonly StyledProperty FormatKeyProperty = + AvaloniaProperty.Register(nameof(FormatKey), null); + + public object FormatKey + { + get => GetValue(FormatKeyProperty); + set => SetValue(FormatKeyProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty FormatProperty = + AvaloniaProperty.Register(nameof(Format), null); + + public string Format + { + get => GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } + + + static TextFormatter() + { + FormatterProperty.Changed.AddClassHandler(FormatterProperty_Changed); + FormatKeyProperty.Changed.AddClassHandler(ResourceKeyProperty_Changed); + } + + static void FormatterProperty_Changed(TextBlock arg1, AvaloniaPropertyChangedEventArgs arg2) + { + throw new NotImplementedException(); + } + + static void ResourceKeyProperty_Changed(TextFormatter s, AvaloniaPropertyChangedEventArgs args) + => s?.OnResourceKeyPropertyChanged(args); + + IDisposable _prevBinding = null; + void OnResourceKeyPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + _prevBinding?.Dispose(); + + + var newKey = args.NewValue; + if (newKey == null) + return; + + var newBinding = this.Bind(FormatProperty, new DynamicResourceExtension(newKey)); + _prevBinding = newBinding; + } + + + static bool TryGetAsBoolean(object obj, out bool value) + { + if (obj == null) + { + value = default; + return false; + } + + if (obj is bool val) + { + value = val; + return true; + } + + string objStr = (obj is string str) + ? str + : obj.ToString() + ; + + return bool.TryParse(objStr, out value); + } + + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (!(values[0] is TextBlock avObj)) + return BindingOperations.DoNothing; + + object[] args = values + .Skip(1) + .ToArray() + ; + + return string.Format(Format, args); + } + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Attached/WindowChromeAddon.cs b/ReCap.CommonUI/Attached/WindowChromeAddon.cs new file mode 100755 index 0000000..b45ad1e --- /dev/null +++ b/ReCap.CommonUI/Attached/WindowChromeAddon.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Avalonia; +using Avalonia.Controls; + +namespace ReCap.CommonUI +{ + public partial class WindowChromeAddon + : AvaloniaObject + { + public static readonly AttachedProperty ManagedShowTitleProperty = + AvaloniaProperty.RegisterAttached("ManagedShowTitle", true); + public static bool GetManagedShowTitle(Window control) + => control.GetValue(ManagedShowTitleProperty); + public static void SetManagedShowTitle(Window control, bool value) + => control.SetValue(ManagedShowTitleProperty, value); + + + + + static bool DefaultToLeftSideButtons => OSInfo.IsMacOS; + public static readonly AttachedProperty LeftSideButtonsProperty = + AvaloniaProperty.RegisterAttached("LeftSideButtons", DefaultToLeftSideButtons); + public static bool GetLeftSideButtons(Window control) + => control.GetValue(LeftSideButtonsProperty); + public static void SetLeftSideButtons(Window control, bool value) + => control.SetValue(LeftSideButtonsProperty, value); + + + + + public static readonly AttachedProperty UseTransparencyHackHintProperty = + AvaloniaProperty.RegisterAttached("UseTransparencyHackHint", false); + public static bool GetUseTransparencyHackHint(Window control) + => control.GetValue(UseTransparencyHackHintProperty); + public static void SetUseTransparencyHackHint(Window control, bool value) + => control.SetValue(UseTransparencyHackHintProperty, value); + + + + + public static readonly AttachedProperty ActualUseTransparencyHackProperty = + AvaloniaProperty.RegisterAttached("ActualUseTransparencyHack", false); + public static bool GetActualUseTransparencyHack(Window control) + => control.GetValue(ActualUseTransparencyHackProperty); + static void SetActualUseTransparencyHack(Window control, bool value) + => control.SetValue(ActualUseTransparencyHackProperty, value); + + + + + static WindowChromeAddon() + { + UseTransparencyHackHintProperty.Changed.AddClassHandler(UseTransparencyHackHintProperty_Changed); + ActualUseTransparencyHackProperty.Changed.AddClassHandler(ActualUseTransparencyHackProperty_Changed); + } + } +} diff --git a/ReCap.CommonUI/Attached/WindowChromeAddon`SWCA.cs b/ReCap.CommonUI/Attached/WindowChromeAddon`SWCA.cs new file mode 100755 index 0000000..e6ab856 --- /dev/null +++ b/ReCap.CommonUI/Attached/WindowChromeAddon`SWCA.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Threading; +using static ReCap.CommonUI.WinUnmanagedMethods; + +namespace ReCap.CommonUI +{ + public partial class WindowChromeAddon + : AvaloniaObject + { + static readonly Version _WIN8_0 = new Version(6, 2, 9200); + static readonly Version _WIN8_1 = new Version(6, 3, 9600); + static readonly bool _ACTUALLY_USE_WIN8_TRANSPARENCY_HACK = Win8_ActuallyUseTransparencyHack(); + static bool Win8_ActuallyUseTransparencyHack() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return false; + + Version osVersion = OSInfo.Version; + if (osVersion.Major > 6) + return false; + + return osVersion >= (OSInfo.IsVersionDefinitelyAccurate ? _WIN8_0 : _WIN8_1); + } + static void UseTransparencyHackHintProperty_Changed(Window sender, AvaloniaPropertyChangedEventArgs e) + { + if (!_ACTUALLY_USE_WIN8_TRANSPARENCY_HACK) + return; + + bool newValue = e.GetNewValue(); + if (GetActualUseTransparencyHack(sender) != newValue) + SetActualUseTransparencyHack(sender, newValue); + } + + + static void ActualUseTransparencyHackProperty_Changed(Window sender, AvaloniaPropertyChangedEventArgs e) + { + var newValue = e.GetNewValue(); + OnActualUseTransparencyHackPropertyChanged(sender, newValue); + } + + + static void DwmDontExtendFrameIntoClientArea(IntPtr hWnd) + { + + var margins = new MARGINS() + { + cxLeftWidth = 0, + cyTopHeight = 0, + cxRightWidth = 0, + cyBottomHeight = 0, + }; + var ret = DwmExtendFrameIntoClientArea(hWnd, ref margins); + Debug.WriteLine($"{nameof(DwmExtendFrameIntoClientArea)}: {ret}"); + } + static void OnActualUseTransparencyHackPropertyChanged(Window sender, bool newValue) + { + if (!sender.TryGetHWnd(out IntPtr hWnd)) + return; + + if (SetWindowCompositionAttributeForTransparency(hWnd, newValue)) + { + Debug.WriteLine("SafeSetWindowCompositionAttribute"); + Dispatcher.UIThread.Post(() => DwmDontExtendFrameIntoClientArea(hWnd)); + } + } + + static bool SetWindowCompositionAttributeForTransparency(IntPtr hWnd, bool transparent) + { + var accent = new AccentPolicy(); + var accentStructSize = Marshal.SizeOf(accent); + + accent.AccentState = transparent + ? AccentState.ACCENT_ENABLE_BLURBEHIND_BUT_ITS_PER_PIXEL_ALPHA_ON_WINDOWS_8 + : AccentState.ACCENT_DISABLED + ; + + var accentPtr = Marshal.AllocHGlobal(accentStructSize); + Marshal.StructureToPtr(accent, accentPtr, false); + + var data = new WindowCompositionAttributeData + { + Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, + SizeOfData = accentStructSize, + Data = accentPtr + }; + return SafeSetWindowCompositionAttribute(hWnd, ref data); + } + static bool SafeSetWindowCompositionAttribute(IntPtr hWnd, ref WindowCompositionAttributeData data) + => SetWindowCompositionAttribute(hWnd, ref data) > 0; + } +} diff --git a/ReCap.CommonUI/Controls/AlignableWrapPanel.cs b/ReCap.CommonUI/Controls/AlignableWrapPanel.cs new file mode 100755 index 0000000..31faf4d --- /dev/null +++ b/ReCap.CommonUI/Controls/AlignableWrapPanel.cs @@ -0,0 +1,129 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Utils; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Visuals; +using Avalonia.VisualTree; +using System; +using System.Collections.Generic; + +namespace ReCap.CommonUI +{ + //https://gist.github.com/gmanny/7450651 + //https://stackoverflow.com/questions/806777/wpf-how-can-i-center-all-items-in-a-wrappanel/7747002#7747002 + public class AlignableWrapPanel : Panel + { + public static readonly StyledProperty HorizontalContentAlignmentProperty = + AvaloniaProperty.Register(nameof(HorizontalContentAlignment), HorizontalAlignment.Left); + + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + + static AlignableWrapPanel() + { + AffectsArrange(HorizontalContentAlignmentProperty); + } + + + protected override Size MeasureOverride(Size constraint) + { + Size curLineSize = new Size(); + Size panelSize = new Size(); + + var children = Children; + + for (int i = 0; i < children.Count; i++) + { + var child = children[i]; + + // Flow passes its own constraint to children + child.Measure(constraint); + Size sz = child.DesiredSize; + + if (curLineSize.Width + sz.Width > constraint.Width) //need to switch to another line + { + panelSize = new Size(Math.Max(curLineSize.Width, panelSize.Width), panelSize.Height + curLineSize.Height); + curLineSize = sz; + + if (sz.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line + { + panelSize = new Size(Math.Max(sz.Width, panelSize.Width), panelSize.Height + sz.Height); + curLineSize = new Size(); + } + } + else //continue to accumulate a line + { + curLineSize = new Size(curLineSize.Width + sz.Width, Math.Max(sz.Height, curLineSize.Height)); + } + } + + // the last line size, if any need to be added + return new Size(Math.Max(curLineSize.Width, panelSize.Width), panelSize.Height + curLineSize.Height); + } + + protected override Size ArrangeOverride(Size arrangeBounds) + { + int firstInLine = 0; + Size curLineSize = new Size(); + double accumulatedHeight = 0; + var children = Children; + + for (int i = 0; i < children.Count; i++) + { + Size sz = children[i].DesiredSize; + + if (curLineSize.Width + sz.Width > arrangeBounds.Width) //need to switch to another line + { + ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i); + + accumulatedHeight += curLineSize.Height; + curLineSize = sz; + + if (sz.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line + { + ArrangeLine(accumulatedHeight, sz, arrangeBounds.Width, i, ++i); + accumulatedHeight += sz.Height; + curLineSize = new Size(); + } + firstInLine = i; + } + else //continue to accumulate a line + { + curLineSize = new Size(curLineSize.Width + sz.Width, Math.Max(sz.Height, curLineSize.Height)); + } + } + + if (firstInLine < children.Count) + ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count); + + return arrangeBounds; + } + + private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end) + { + double x = 0; + if (HorizontalContentAlignment == HorizontalAlignment.Center) + { + x = (boundsWidth - lineSize.Width) / 2; + } + else if (HorizontalContentAlignment == HorizontalAlignment.Right) + { + x = (boundsWidth - lineSize.Width); + } + + var children = Children; + for (int i = start; i < end; i++) + { + var child = children[i]; + child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineSize.Height)); + x += child.DesiredSize.Width; + } + } + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Controls/AngledBorder.cs b/ReCap.CommonUI/Controls/AngledBorder.cs new file mode 100755 index 0000000..811ea91 --- /dev/null +++ b/ReCap.CommonUI/Controls/AngledBorder.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace ReCap.CommonUI +{ + public class AngledBorder + : AngledBorderBase + { + /// + /// Defines the property. + /// + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + /// + /// Gets or sets the radius of the border rounded corners. + /// + public CornerRadius CornerRadius + { + get => GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + static AngledBorder() + { + AffectsGeometry(CornerRadiusProperty); + AffectsRender(CornerRadiusProperty); + } + + protected override void RefreshGeometry(out Geometry fillGeometry, out Geometry strokeGeometry, out RoundedRect glowRect) + { + //Console.WriteLine($"Updating geometries..."); + double width = Math.Round(Bounds.Width); + double height = Math.Round(Bounds.Height); + + double minDimen = Math.Min(width, height); + + CornerRadius radius = CornerRadius; + double tl = Math.Min(radius.TopLeft, minDimen); + double tr = Math.Min(radius.TopRight, minDimen); + double br = Math.Min(radius.BottomRight, minDimen); + double bl = Math.Min(radius.BottomLeft, minDimen); + + /*tl = Math.Round(tl, 0); + tr = Math.Round(tr, 0); + br = Math.Round(br, 0); + bl = Math.Round(bl, 0);*/ + + double strokeThickness = StrokeThickness; + double borderBothSides = strokeThickness * 2; + double fillWidth = width - borderBothSides; + double fillHeight = height - borderBothSides; + var rect = new Rect(strokeThickness, strokeThickness, fillWidth, fillHeight); + glowRect = new RoundedRect(rect, tl, tr, br, bl); + + var fillGeom = new StreamGeometry(); + using (var ctx = fillGeom.Open()) + { + ctx.CreateGeometry(strokeThickness, strokeThickness, fillWidth, fillHeight, tl, tr, br, bl, true); + } + fillGeometry = fillGeom; + + /*double realAverageBorderThickness = _averageBorderThickness; + _averageBorderThickness = 4; + borderBothSides = _averageBorderThickness * 2; + fillWidth = width - borderBothSides; + fillHeight = height - borderBothSides;*/ + + strokeGeometry = null; + if (strokeThickness > 0) + { + double diagonalDiff = strokeThickness / 2; + + tl = (tl > 0) ? Math.Round(tl + diagonalDiff, 0) : 0; + tr = (tr > 0) ? Math.Round(tr + diagonalDiff, 0) : 0; + br = (br > 0) ? Math.Round(br + diagonalDiff, 0) : 0; + bl = (bl > 0) ? Math.Round(bl + diagonalDiff, 0) : 0; + var strokeOuterGeometry = new StreamGeometry(); + using (var ctx = strokeOuterGeometry.Open()) + { + ctx.CreateGeometry(0, 0, width, height, tl, tr, br, bl, true); + //////ctx.BeginFigure(new Point(0 + 1, 0 + tl), true); + //////ctx.TraverseGeometry(0, 0, width, height, tl, tr, br, bl, true); + //ctx.TraverseGeometry(_averageBorderThickness, _averageBorderThickness, fillWidth, fillHeight, tl - _averageBorderThickness, tr - _averageBorderThickness, br - _averageBorderThickness, bl - _averageBorderThickness, false); + //////ctx.EndFigure(true); + } + + //_averageBorderThickness = realAverageBorderThickness; + //strokeGeometry = new CombinedGeometry(GeometryCombineMode.Xor, strokeOuterGeometry, fillGeometry/*, new TranslateTransform(0, 0)*/); + strokeGeometry = strokeOuterGeometry; + } + } + } + + public static partial class AngledCorners + { + public static void CreateGeometry(this StreamGeometryContext ctx, double x, double y, double width, double height, double tl, double tr, double br, double bl, bool isFilled) + { + if (tl > 0) + { + ctx.BeginFigure(new Point(x, y + tl), isFilled); + ctx.LineTo(new Point(x + tl, y)); + } + else + ctx.BeginFigure(new Point(x, y), isFilled); + + //////ctx.TraverseGeometry(x, y, width, height, tl, tr, br, bl, true); + + + if (tr > 0) + { + ctx.LineTo(new Point(x + (width - tr), y)); + ctx.LineTo(new Point(x + width, y + tr)); + } + else + ctx.LineTo(new Point(x + width, y)); + + + if (br > 0) + { + ctx.LineTo(new Point(x + width, y + (height - br))); + ctx.LineTo(new Point(x + (width - br), y + height)); + } + else + ctx.LineTo(new Point(x + width, y + height)); + + + if (bl > 0) + { + ctx.LineTo(new Point(x + bl, y + height)); + ctx.LineTo(new Point(x, y + (height - bl))); + } + else + ctx.LineTo(new Point(x, y + height)); + + + ctx.EndFigure(true); + } + + public static void TraverseGeometry(this StreamGeometryContext ctx, double x, double y, double width, double height, double tl, double tr, double br, double bl, bool outer) + { + List steps = new List() + { + () => ctx.LineTo(new Point(x + tl, y)), + () => ctx.LineTo(new Point(x + (width - tr), y)), + () => ctx.LineTo(new Point(x + width, y + tr)), + () => ctx.LineTo(new Point(x + width, y + (height - br))), + () => ctx.LineTo(new Point(x + (width - br), y + height)), + () => ctx.LineTo(new Point(x + bl, y + height)), + () => ctx.LineTo(new Point(x, y + (height - bl))), + }; + if (!outer) + steps.Insert(0, () => new Point(x, y + tl)); + + for (int i = 0; i < steps.Count; i++) + steps[outer ? i : steps.Count - (i)](); + } + /*public static void CreateGeometry(this StreamGeometryContext ctx, double width, double height, double tl, double tr, double br, double bl, bool isFilled) + { + ctx.BeginFigure(new Point(1, tl), isFilled); + ctx.LineTo(new Point(tl, 1)); + ctx.LineTo(new Point(width - tr, 1)); + ctx.LineTo(new Point(width, tr)); + ctx.LineTo(new Point(width, height - br)); + ctx.LineTo(new Point(width - br, height)); + ctx.LineTo(new Point(bl, height)); + ctx.LineTo(new Point(1, height - bl)); + ctx.EndFigure(true); + }*/ + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Controls/AngledBorderBase.cs b/ReCap.CommonUI/Controls/AngledBorderBase.cs new file mode 100755 index 0000000..ed1630c --- /dev/null +++ b/ReCap.CommonUI/Controls/AngledBorderBase.cs @@ -0,0 +1,317 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; + +namespace ReCap.CommonUI +{ + public abstract class AngledBorderBase : Decorator + { + const int BLUR_SPREAD_OFFSET = 100; + const int BLUR_SPREAD_AXIS_OFFSET = (int)(BLUR_SPREAD_OFFSET/* * 1.5*/); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundProperty = + Border.BackgroundProperty.AddOwner(); + /// + /// Gets or sets a brush with which to paint the background. + /// + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderBrushProperty = + Border.BorderBrushProperty.AddOwner(); + /// + /// Gets or sets a brush with which to paint the border. + /// + public IBrush BorderBrush + { + get => GetValue(BorderBrushProperty); + set => SetValue(BorderBrushProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeThicknessProperty = + Shape.StrokeThicknessProperty.AddOwner(); + /// + /// Gets or sets the width of the shape outline. + /// + public double StrokeThickness + { + get => GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty InnerGlowSizeProperty = + AvaloniaProperty.Register(nameof(InnerGlowSize), -1); + + public double InnerGlowSize + { + get => GetValue(InnerGlowSizeProperty); + set => SetValue(InnerGlowSizeProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty InnerGlowColorProperty = + AvaloniaProperty.Register(nameof(InnerGlowColor)); + + public Color InnerGlowColor + { + get => GetValue(InnerGlowColorProperty); + set => SetValue(InnerGlowColorProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly DirectProperty FillClipBindableProperty = + AvaloniaProperty.RegisterDirect(nameof(FillClipBindable), o => o._fillGeometry); + + public Geometry FillClipBindable + { + get => GetValue(FillClipBindableProperty); + set => SetValue(FillClipBindableProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly DirectProperty StrokeClipBindableProperty = + AvaloniaProperty.RegisterDirect(nameof(StrokeClipBindable), o => o._strokeGeometry); + + public Geometry StrokeClipBindable + { + get => GetValue(StrokeClipBindableProperty); + set => SetValue(StrokeClipBindableProperty, value); + } + + + + + BoxShadow _boxShadow = new BoxShadow() + { + OffsetX = 0, + OffsetY = 0, + IsInset = true, + }; + //BoxShadows _boxShadows = new BoxShadows(); + static readonly Geometry _DEFAULT_GEOMETRY = new StreamGeometry(); + Geometry _fillGeometry = new StreamGeometry(); + Geometry _strokeGeometry = new StreamGeometry(); +#if DEBUG_ANGLED_BORDER + Geometry _strokeGeometryOuter = null; +#endif + RoundedRect _glowRect = new RoundedRect(new Rect(0, 0, 3, 3), 0); + + Thickness _strokeThickness = new Thickness(0); + + + + static AngledBorderBase() + { + Extensions.MakeControlTypeNonInteractive(); + + StrokeThicknessProperty.Changed.AddClassHandler((s, e) => + { + s._strokeThickness = new Thickness(e.GetNewValue()); + AffectsGeometryInvalidate(s, e); + }); + + InnerGlowColorProperty.Changed.AddClassHandler(InnerGlowColorProperty_Changed); + //((s, e) => s._boxShadow.Color = ((e.NewValue != null) && (e.NewValue is Color color)) ? color : Colors.Transparent); + InnerGlowSizeProperty.Changed.AddClassHandler(InnerGlowSizeProperty_Changed); + //((s, e) => s._boxShadow.Blur = ((e.NewValue != null) && (e.NewValue is double radius)) ? radius : -1); + + + AffectsRender( + BackgroundProperty, + BorderBrushProperty, + StrokeThicknessProperty, + InnerGlowColorProperty, + InnerGlowSizeProperty); + } + + + static void InnerGlowColorProperty_Changed(AngledBorderBase s, AvaloniaPropertyChangedEventArgs e) + => s._boxShadow.Color = e.TryGetNewValue(out Color color) + ? color + : Colors.Transparent + ; + static void InnerGlowSizeProperty_Changed(AngledBorderBase s, AvaloniaPropertyChangedEventArgs e) + => s._boxShadow.Blur = e.TryGetNewValue(out double radius) + ? radius + : -1 + ; + + + /// + /// Marks a property as affecting the border's geometry. + /// + /// The properties. + /// + /// After a call to this method in a control's static constructor, any change to the + /// property will cause to be called on the element. + /// + protected static void AffectsGeometry(params AvaloniaProperty[] properties) + where TAngledBorder : AngledBorderBase + { + foreach (var property in properties) + { + property.Changed.Subscribe(e => + { + if (e.Sender is TAngledBorder shape) + { + AffectsGeometryInvalidate(shape, e); + } + }); + } + } + + private static void AffectsGeometryInvalidate(TAngledBorder sender, AvaloniaPropertyChangedEventArgs e) + where TAngledBorder : AngledBorderBase + { + sender.InvalidateGeometry(); + } + + + public AngledBorderBase() : base() + { + //_boxShadows = new BoxShadows(_boxShadow); + + var ctrl = (this as Control); + + ctrl.LayoutUpdated += (s, e) => InvalidateGeometry(); + ctrl.AttachedToVisualTree += (s, e) => InvalidateGeometry(); + } + + void OnBorderThicknessChanged(AvaloniaPropertyChangedEventArgs e) + { + var nv = e.NewValue; + //_averageBorderThickness = ((nv != null) && (nv is Thickness brdThck)) ? ((brdThck.Left + brdThck.Top + brdThck.Right + brdThck.Bottom) / 4) : 0; + } + + protected void InvalidateGeometry() + { + var prevFillGeometry = _fillGeometry; + RefreshGeometry(out _fillGeometry, out Geometry strokeGeometryOuter, out RoundedRect glowRect); + if (prevFillGeometry != _fillGeometry) + RaisePropertyChanged(FillClipBindableProperty, prevFillGeometry, _fillGeometry); + + var prevStrokeGeometry = _strokeGeometry; + if (strokeGeometryOuter != null) + _strokeGeometry = new CombinedGeometry(GeometryCombineMode.Xor, strokeGeometryOuter, _fillGeometry); + else + _strokeGeometry = null; +#if DEBUG_ANGLED_BORDER + _strokeGeometryOuter = strokeGeometryOuter; +#endif + if (prevStrokeGeometry != _strokeGeometry) + { + var newStrokeGeometry = _strokeGeometry != null + ? _strokeGeometry + : _DEFAULT_GEOMETRY + ; + RaisePropertyChanged(StrokeClipBindableProperty, prevStrokeGeometry, newStrokeGeometry); + } + + //rrect.Rect + //_rrect = new RoundedRect(rrect.Rect.Inflate(BLUR_SPREAD_OFFSET), rrect.RadiiTopLeft, rrect.RadiiTopRight, rrect.RadiiBottomRight, rrect.RadiiBottomLeft); + _glowRect = glowRect.Inflate(BLUR_SPREAD_OFFSET, BLUR_SPREAD_OFFSET); + + + double minDimen = InnerGlowSize; + double width = Math.Round(Bounds.Width); + double height = Math.Round(Bounds.Height); + + minDimen = ((minDimen >= 0) ? minDimen : 1) * Math.Min(width, height); + + /*double blurBase = minDimen / 2; + double spreadBase = minDimen / 3; + _boxShadow.Blur = blurBase + spreadBase; + _boxShadow.Spread = spreadBase;*/ + + double blurBase = minDimen / 2; + double spreadBase = minDimen / 9; + _boxShadow.Blur = blurBase - spreadBase; + _boxShadow.Spread = spreadBase + BLUR_SPREAD_OFFSET; + } + + protected abstract void RefreshGeometry(out Geometry fillGeometry, out Geometry strokeGeometry, out RoundedRect glowRect); + + + static readonly IBrush DEBUG_BRUSH = new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x00, 0x00)); + static readonly IBrush DEBUG_BRUSH_2 = new SolidColorBrush(Color.FromArgb(0x80, 0x00, 0x00, 0xFF)); + static readonly IBrush DEBUG_BRUSH_3 = new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0xFF, 0x00)); + public override void Render(DrawingContext context) + { +#if DEBUG_ANGLED_BORDER + context.DrawGeometry(DEBUG_BRUSH, null, _strokeGeometryOuter); + context.DrawGeometry(DEBUG_BRUSH_2, null, _fillGeometry); + + using (var what = context.PushGeometryClip(_fillGeometry)) + { + //context.DrawRectangle(DEBUG_BRUSH, null, _rrect.Rect); + context.DrawGeometry(DEBUG_BRUSH_3, null, _strokeGeometry); + } +#else + //Console.WriteLine($"Rendering...\n\t_fillGeometry?: {_fillGeometry != null}\n\t_strokeGeometry?: {_strokeGeometry != null}"); + + if (Background != null) + context.DrawGeometry(Background, null, _fillGeometry); + + using (var idk2 = context.PushGeometryClip(_fillGeometry)) + { + context.DrawRectangle(null, null, _glowRect, new BoxShadows(_boxShadow)); + } + + + if (StrokeThickness > 0) + { + context.DrawGeometry + ( + BorderBrush //null + , null/*new Pen() + { + Brush = , + Thickness = _averageBorderThickness + }*/ + , _strokeGeometry + ); + } +#endif + } + + static readonly Thickness ZERO = new Thickness(0); + protected override Size MeasureOverride(Size availableSize) + { + return LayoutHelper.MeasureChild(Child, availableSize, Padding, ZERO); + } + + protected override Size ArrangeOverride(Size finalSize) + { + return LayoutHelper.ArrangeChild(Child, finalSize, Padding, ZERO); + } + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Controls/AngledBorderEx.cs b/ReCap.CommonUI/Controls/AngledBorderEx.cs new file mode 100755 index 0000000..e017f77 --- /dev/null +++ b/ReCap.CommonUI/Controls/AngledBorderEx.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Media; + +namespace ReCap.CommonUI +{ + public enum InvertedSide + { + None = -1, + Bottom = 1, + Left = 0, + Right = 2, + Top = 3 + } + + public partial class AngledBorderEx + : AngledBorderBase + { + /// + /// Defines the property. + /// + public static readonly StyledProperty TopLeftCutProperty = + AvaloniaProperty.Register(nameof(TopLeftCut), 0); + /// + /// Gets or sets the size of the cut corner + /// + public double TopLeftCut + { + get => GetValue(TopLeftCutProperty); + set => SetValue(TopLeftCutProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty TopRightCutProperty = + AvaloniaProperty.Register(nameof(TopRightCut), 0); + /// + /// Gets or sets the size of the cut corner + /// + public double TopRightCut + { + get => GetValue(TopRightCutProperty); + set => SetValue(TopRightCutProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty BottomRightCutProperty = + AvaloniaProperty.Register(nameof(BottomRightCut), 0); + /// + /// Gets or sets the size of the cut corner + /// + public double BottomRightCut + { + get => GetValue(BottomRightCutProperty); + set => SetValue(BottomRightCutProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty BottomLeftCutProperty = + AvaloniaProperty.Register(nameof(BottomLeftCut), 0); + /// + /// Gets or sets the size of the cut corner + /// + public double BottomLeftCut + { + get => GetValue(BottomLeftCutProperty); + set => SetValue(BottomLeftCutProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty TopLeftInsetProperty = + AvaloniaProperty.Register(nameof(TopLeftInset), 0); + /// + /// Gets or sets the inset distance of the cut corner + /// + public double TopLeftInset + { + get => GetValue(TopLeftInsetProperty); + set => SetValue(TopLeftInsetProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty TopRightInsetProperty = + AvaloniaProperty.Register(nameof(TopRightInset), 0); + /// + /// Gets or sets the inset distance of the cut corner + /// + public double TopRightInset + { + get => GetValue(TopRightInsetProperty); + set => SetValue(TopRightInsetProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty BottomRightInsetProperty = + AvaloniaProperty.Register(nameof(BottomRightInset), 0); + /// + /// Gets or sets the inset distance of the cut corner + /// + public double BottomRightInset + { + get => GetValue(BottomRightInsetProperty); + set => SetValue(BottomRightInsetProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty BottomLeftInsetProperty = + AvaloniaProperty.Register(nameof(BottomLeftInset), 0); + /// + /// Gets or sets the inset distance of the cut corner + /// + public double BottomLeftInset + { + get => GetValue(BottomLeftInsetProperty); + set => SetValue(BottomLeftInsetProperty, value); + } + + + /// + /// Defines the property. + /// + public static readonly StyledProperty InvertSideProperty = + AvaloniaProperty.Register(nameof(InvertSide), InvertedSide.None); + /// + /// Gets or sets the inset distance of the cut corner + /// + public InvertedSide InvertSide + { + get => GetValue(InvertSideProperty); + set => SetValue(InvertSideProperty, value); + } + + + static AngledBorderEx() + { + AvaloniaProperty[] props = + { + TopLeftCutProperty, + TopLeftInsetProperty, + TopRightCutProperty, + TopRightInsetProperty, + BottomRightCutProperty, + BottomRightInsetProperty, + BottomLeftCutProperty, + BottomLeftInsetProperty, + InvertSideProperty + }; + + + AffectsGeometry(props); + AffectsRender(props); + } + + protected override void RefreshGeometry(out Geometry fillGeometry, out Geometry strokeGeometry, out RoundedRect glowRect) + { + //Console.WriteLine($"Updating geometries..."); + double width = Math.Round(Bounds.Width); + double height = Math.Round(Bounds.Height); + + double minDimen = Math.Min(width, height); + + double tl = 0; + double tr = 0; + double br = 0; + double bl = 0; + + double tlInset = 0; + double trInset = 0; + double brInset = 0; + double blInset = 0; + + + tl = Math.Min(TopLeftCut, minDimen); + tr = Math.Min(TopRightCut, minDimen); + br = Math.Min(BottomRightCut, minDimen); + bl = Math.Min(BottomLeftCut, minDimen); + + tlInset = Math.Min(TopLeftInset, width); + trInset = Math.Min(TopRightInset, width); + brInset = Math.Min(BottomRightInset, width); + blInset = Math.Min(BottomLeftInset, width); + /*tlInset = Math.Min(TopLeftInset, minDimen - tl); + trInset = Math.Min(TopRightInset, minDimen - tr); + brInset = Math.Min(BottomRightInset, minDimen - br); + blInset = Math.Min(BottomLeftInset, minDimen - bl);*/ + + /*tl = Math.Round(tl, 0); + tr = Math.Round(tr, 0); + br = Math.Round(br, 0); + bl = Math.Round(bl, 0);*/ + + double strokeThickness = StrokeThickness; + bool hasStroke = strokeThickness > 0; + //double strokeHalf = hasStroke ? (strokeThickness / 2) : 0; + double borderBothSides = strokeThickness * 2; + + double fillWidth = width - borderBothSides; + double fillHeight = height - borderBothSides; + + var rect = new Rect(strokeThickness, strokeThickness, fillWidth, fillHeight); + Thickness outsetTh = default(Thickness); + + var fillGeom = new StreamGeometry(); + using (var ctx = CreateGeometry( + ref fillGeom, + /*strokeThickness, + strokeThickness, + fillWidth, + fillHeight,*/ + 0, + 0, + width, + height, + + tl, + tr, + br, + bl, + + tlInset, + trInset, + brInset, + blInset, + + InvertSide, + strokeThickness, false, + out outsetTh) + ) + { + fillGeometry = fillGeom; + + double prX = rect.X + outsetTh.Left; + double prY = rect.Y + outsetTh.Top; + double prWidth = rect.Width + (outsetTh.Right - outsetTh.Left); //(outsetTh.Left + outsetTh.Right); + double prHeight = rect.Height + (outsetTh.Bottom - outsetTh.Top); //(outsetTh.Top + outsetTh.Bottom); + var preRoundedRect = + //rect.Deflate(outsetTh).Inflate(outsetTh) + new Rect(prX, prY, prWidth, prHeight) + //.Deflate(25) + //new Rect(rect.Left - Math.Min(0, outsetTh.Left), rect.Top - Math.Min(0, outsetTh.Top), rect.Right + Math.Min(0, outsetTh.Right), rect.Bottom + Math.Min(0, outsetTh.Bottom)) + //new Rect(rect.Left + outsetTh.Left, rect.Top + outsetTh.Top, rect.Width - (outsetTh.Left + outsetTh.Right), rect.Height - (outsetTh.Top + outsetTh.Bottom)) + //rect + ; + var roundedRect = new RoundedRect( + preRoundedRect, + 0, 0, 0, 0 + //tl, tr, br, bl + ); + /*double realAverageBorderThickness = _averageBorderThickness; + _averageBorderThickness = 4; + borderBothSides = _averageBorderThickness * 2; + fillWidth = width - borderBothSides; + fillHeight = height - borderBothSides;*/ + + strokeGeometry = null; + if (strokeThickness > 0) + { + /*double diagonalDiff = Math.Round(strokeThickness / 2, MidpointRounding.AwayFromZero); + double diagonalDiffSm = strokeThickness - diagonalDiff; //(int)diagonalDiff; + //strokeThickness / 2; + + tl = (tl > 0) ? tl + diagonalDiff : 0; + tr = (tr > 0) ? tr + diagonalDiff : 0; + br = (br > 0) ? br + diagonalDiff : 0; + bl = (bl > 0) ? bl + diagonalDiff : 0; + + tlInset = (tlInset > 0) ? tlInset + diagonalDiff : 0; + trInset = (trInset > 0) ? trInset + diagonalDiff : 0; + brInset = (brInset > 0) ? brInset + diagonalDiff : 0; + blInset = (blInset > 0) ? blInset + diagonalDiff : 0;*/ + var strokeOuterGeometry = new StreamGeometry(); + using (var ctx2 = CreateGeometry( + ref strokeOuterGeometry, + 0, + 0, + width, + height, + + tl, + tr, + br, + bl, + + tlInset, + trInset, + brInset, + blInset, + + InvertSide, + strokeThickness, true, + out _)) + { + //_averageBorderThickness = realAverageBorderThickness; + //strokeGeometry = new CombinedGeometry(GeometryCombineMode.Xor, strokeOuterGeometry, fillGeometry/*, new TranslateTransform(0, 0)*/); + strokeGeometry = strokeOuterGeometry; + } + } + + glowRect = roundedRect; + } + } + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Controls/AngledBorderEx`createGeom.cs b/ReCap.CommonUI/Controls/AngledBorderEx`createGeom.cs new file mode 100755 index 0000000..4cc2329 --- /dev/null +++ b/ReCap.CommonUI/Controls/AngledBorderEx`createGeom.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Media; + +namespace ReCap.CommonUI +{ + public partial class AngledBorderEx + : AngledBorderBase + { + static StreamGeometryContext CreateGeometry( + ref StreamGeometry streamGeom, + + double xRaw, double yRaw, double widthRaw, double heightRaw, + + double tlRaw, double trRaw, double brRaw, double blRaw, + double tlInsetRaw, double trInsetRaw, double brInsetRaw, double blInsetRaw, + + InvertedSide invSide, + double strokeThicknessRaw, bool isForStroke, + out Thickness outset) + { + StreamGeometryContext ctx = streamGeom.Open(); + + bool noXYInvert = invSide == InvertedSide.None; + + double x = xRaw; + double y = yRaw; + double width = widthRaw; + double height = heightRaw; + + double tl = tlRaw; + double tr = trRaw; + double br = brRaw; + double bl = blRaw; + + double tlInset = tlInsetRaw; + double trInset = trInsetRaw; + double brInset = brInsetRaw; + double blInset = blInsetRaw; + + double strokeThickness = strokeThicknessRaw; + double strokeBothSides = strokeThickness * 2; + bool hasStroke = strokeThicknessRaw > 0; + + bool isInvLeft = invSide == InvertedSide.Left; + bool isInvTop = invSide == InvertedSide.Top; + bool isInvRight = invSide == InvertedSide.Right; + bool isInvBottom = invSide == InvertedSide.Bottom; + + if (hasStroke && (!isForStroke)) + { + x += strokeThickness; + y += strokeThickness; + width -= strokeBothSides; + height -= strokeBothSides; + + double invInsetDelta = strokeThickness * 1.41425; + if (isInvLeft) + { + tlInset = 0; + blInset = 0; + + x += strokeThickness; + width -= strokeThickness; + + tl -= strokeThickness; + bl -= strokeThickness; + } + else if (isInvTop) + { + bool lNonZero = tlInset > 0; + bool rNonZero = trInset > 0; + + if (lNonZero) + tlInset -= invInsetDelta; + else + tl -= strokeThickness; + + if (rNonZero) + trInset -= invInsetDelta; + else + tr -= strokeThickness; + + if (!(lNonZero || rNonZero)) + { + y += strokeThickness; + height -= strokeThickness; + } + } + else if (isInvRight) + { + trInset = 0; + brInset = 0; + + width -= strokeThickness; + + tr -= strokeThickness; + br -= strokeThickness; + } + else if (isInvBottom) + { + bool rNonZero = brInset > 0; + bool lNonZero = blInset > 0; + + if (rNonZero) + brInset -= invInsetDelta; + else + br -= strokeThickness; + + if (lNonZero) + blInset -= invInsetDelta; + else + bl -= strokeThickness; + + if (!(rNonZero || lNonZero)) + { + height -= strokeThickness; + } + } + } + + if (tlRaw <= 0) + tl = 0; + + if (trRaw <= 0) + tr = 0; + + if (brRaw <= 0) + br = 0; + + if (blRaw <= 0) + bl = 0; + + double leftOutset = 0; + bool leftInvert = false; + + double topOutset = 0; + bool topInvert = false; + + double rightOutset = 0; + bool rightInvert = false; + + double bottomOutset = 0; + bool bottomInvert = false; + + if (!noXYInvert) + { + if (isInvRight) + { + rightInvert = true; + rightOutset = Math.Max(tr, br); + trInset = 0; + brInset = 0; + } + else if (isInvTop) + { + topInvert = true; + topOutset = Math.Max(tl, tr); + } + else if (isInvBottom) + { + bottomInvert = true; + bottomOutset = Math.Max(bl, br); + } + else if (isInvLeft) + { + leftInvert = true; + leftOutset = Math.Max(tl, bl); + tlInset = 0; + blInset = 0; + } + } + + double nearX = x + leftOutset; + double nearY = y + topOutset; + double farX = (x + width) - rightOutset; + double farY = (y + height) - bottomOutset; + + rightInvert = !rightInvert; + bottomInvert = !bottomInvert; + + + Point outerPoint; + + bool hasInsetPoint; + Point insetPoint; + + bool hasCutPoint; + Point cutPoint; + + + //top left + AngledBorderUtils.ResolveCorner(tl, tlInset, nearX, nearY, leftInvert, topInvert + , out outerPoint, out hasInsetPoint, out insetPoint, out hasCutPoint, out cutPoint); + ctx.BeginFigure(outerPoint, true); + if (hasInsetPoint) + ctx.LineTo(insetPoint); + if (hasCutPoint) + ctx.LineTo(cutPoint); + + + //top right + AngledBorderUtils.ResolveCorner(tr, trInset, farX, nearY, rightInvert, topInvert + , out outerPoint, out hasInsetPoint, out insetPoint, out hasCutPoint, out cutPoint); + if (hasCutPoint) + ctx.LineTo(cutPoint); + if (hasInsetPoint) + ctx.LineTo(insetPoint); + ctx.LineTo(outerPoint); + + + //bottom right + AngledBorderUtils.ResolveCorner(br, brInset, farX, farY, rightInvert, bottomInvert + , out outerPoint, out hasInsetPoint, out insetPoint, out hasCutPoint, out cutPoint); + ctx.LineTo(outerPoint); + if (hasInsetPoint) + ctx.LineTo(insetPoint); + if (hasCutPoint) + ctx.LineTo(cutPoint); + + + //bottom left + AngledBorderUtils.ResolveCorner(bl, blInset, nearX, farY, leftInvert, bottomInvert + , out outerPoint, out hasInsetPoint, out insetPoint, out hasCutPoint, out cutPoint); + if (hasCutPoint) + ctx.LineTo(cutPoint); + if (hasInsetPoint) + ctx.LineTo(insetPoint); + ctx.LineTo(outerPoint); + + + ctx.EndFigure(true); + + outset = new Thickness( + leftInvert ? leftOutset : -leftOutset + , topInvert ? topOutset : -topOutset + , rightInvert ? rightOutset : -rightOutset + , bottomInvert ? bottomOutset : -bottomOutset); + + return ctx; + } + } +} \ No newline at end of file diff --git a/ReCap.CommonUI/Controls/AngledBorderUtils.cs b/ReCap.CommonUI/Controls/AngledBorderUtils.cs new file mode 100755 index 0000000..6a3c597 --- /dev/null +++ b/ReCap.CommonUI/Controls/AngledBorderUtils.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using Avalonia; + +namespace ReCap.CommonUI +{ + internal enum BoundsPortion : int + { + Left = 0b0001, + Bottom = 0b0010, + Right = 0b0100, + Top = 0b1000, + + TopLeft = Top | Left, + TopRight = Top | Right, + BottomRight = Bottom | Right, + BottomLeft = Bottom | Left, + } + internal static partial class AngledBorderUtils + { + static void AddInOrder(IEnumerable addFrom, ref List points) + { + foreach (var pt in addFrom) + { + points.Add(pt); + } + } + + static void AddReversed(IList addFrom, ref List points) + { + int lastPointIndex = addFrom.Count - 1; + for (int i = lastPointIndex; i >= 0; i--) + { + points.Add(addFrom[i]); + } + } + + + + + public static void ResolveCorner(double cut, double inset, double startX, double startY, bool xInvert, bool yInvert, ref List points) + { + if (cut > 0) + { + int xMult = xInvert ? -1 : 1; + int yMult = yInvert ? -1 : 1; + + double xInner = cut * xMult; + double yInner = cut * yMult; + double xInset = inset * xMult; + + double x = startX; + double y = startY; + y += yInner; + + points.Add(new Point(x, y)); + if (inset > 0) + { + x += xInset; + points.Add(new Point(x, y)); + } + /*else if (inset < 0) + { + + }*/ + /*else + {*/ + //points.Add(new Point(x, startX + xInner)); + //points.Add(new Point(startY + yInner, y)); + x += xInner; + y -= yInner; + points.Add(new Point(x, y)); + //} + } + else + points.Add(new Point(startX, startY)); + } + + /*public static void TraverseGeometry(this StreamGeometryContext ctx, double x, double y, double width, double height, double tl, double tr, double br, double bl, double tlInset, double trInset, double brInset, double blInset, bool outer) + { + if + ( + (tlInset == 0) + && (trInset == 0) + && (blInset == 0) + && (brInset == 0) + ) + { + CreateGeometry(ctx, x, y, width, height, tl, tr, br, bl, outer); + return; + } + + + List steps = new List() + { + () => ctx.LineTo(new Point(x + tl, y)), + () => ctx.LineTo(new Point(x + (width - tr), y)), + () => ctx.LineTo(new Point(x + width, y + tr)), + () => ctx.LineTo(new Point(x + width, y + (height - br))), + () => ctx.LineTo(new Point(x + (width - br), y + height)), + () => ctx.LineTo(new Point(x + bl, y + height)), + () => ctx.LineTo(new Point(x, y + (height - bl))), + }; + if (!outer) + steps.Insert(0, () => new Point(x, y + tl)); + + for (int i = 0; i < steps.Count; i++) + steps[outer ? i : steps.Count - (i)](); + }*/ + /*public static void CreateGeometry(this StreamGeometryContext ctx, double width, double height, double tl, double tr, double br, double bl, bool isFilled) + { + ctx.BeginFigure(new Point(1, tl), isFilled); + ctx.LineTo(new Point(tl, 1)); + ctx.LineTo(new Point(width - tr, 1)); + ctx.LineTo(new Point(width, tr)); + ctx.LineTo(new Point(width, height - br)); + ctx.LineTo(new Point(width - br, height)); + ctx.LineTo(new Point(bl, height)); + ctx.LineTo(new Point(1, height - bl)); + ctx.EndFigure(true); + }*/ + + + + public static void ResolveCorner(double cut, double inset, double startX, double startY, BoundsPortion corner, bool verticalInvert + , out Point outerPoint, out bool hasInsetPoint, out Point insetPoint, out bool hasCutPoint, out Point cutPoint) + { + bool xInvert = corner.HasFlag(BoundsPortion.Right); + bool yInvert = corner.HasFlag(BoundsPortion.Bottom); + + ResolveCorner(cut, inset, startX, startY, xInvert + , verticalInvert + ? !yInvert + : yInvert + , out outerPoint, out hasInsetPoint, out insetPoint, out hasCutPoint, out cutPoint) + ; + } + public static void ResolveCorner(double cut, double inset, double startX, double startY, bool xInvert, bool yInvert + , out Point outerPoint, out bool hasInsetPoint, out Point insetPoint, out bool hasCutPoint, out Point cutPoint) + { + if (cut <= 0) + { + outerPoint = new Point(startX, startY); + hasInsetPoint = false; + insetPoint = default; + hasCutPoint = false; + cutPoint = default; + return; + } + + int xMult = xInvert ? -1 : 1; + int yMult = yInvert ? -1 : 1; + + double xInner = cut * xMult; + double yInner = cut * yMult; + double xInset = inset * xMult; + + double x = startX; + double y = startY; + y += yInner; + + outerPoint = new Point(x, y); + if (inset > 0) + { + x += xInset; + insetPoint = new Point(x, y); + hasInsetPoint = true; + } + else + { + insetPoint = default; + hasInsetPoint = false; + } + + x += xInner; + y -= yInner; + cutPoint = new Point(x, y); + hasCutPoint = true; + } + } +} diff --git a/ReCap.CommonUI/Controls/CaptionButton.cs b/ReCap.CommonUI/Controls/CaptionButton.cs new file mode 100755 index 0000000..62ca645 --- /dev/null +++ b/ReCap.CommonUI/Controls/CaptionButton.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; + +namespace ReCap.CommonUI +{ + public class CaptionButton + : Button + { + public static readonly DirectProperty IsActiveProperty + = Window.IsActiveProperty.AddOwner(s => s.IsActive, (s, v) => s.IsActive = v); + + bool _isActive; + public bool IsActive + { + get => _isActive; + private set => SetAndRaise(IsActiveProperty, ref _isActive, value); + } + static CaptionButton() + { + AffectsRender(IsActiveProperty); + } + } +} diff --git a/ReCap.CommonUI/Controls/CaptionButtonsSizeBridge.cs b/ReCap.CommonUI/Controls/CaptionButtonsSizeBridge.cs new file mode 100755 index 0000000..8016873 --- /dev/null +++ b/ReCap.CommonUI/Controls/CaptionButtonsSizeBridge.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia; +using Avalonia.Controls; + +namespace ReCap.CommonUI +{ + public partial class CaptionButtonsSizeBridge + : Panel + { + public static readonly AttachedProperty CaptionButtonsWidthProperty = + AvaloniaProperty.RegisterAttached("CaptionButtonsWidth", 0.0f); + public static double GetCaptionButtonsWidth(Window control) + => control.GetValue(CaptionButtonsWidthProperty); + public static void SetCaptionButtonsWidth(Window control, double value) + => control.SetValue(CaptionButtonsWidthProperty, value); + + + protected override void OnSizeChanged(SizeChangedEventArgs e) + { + base.OnSizeChanged(e); + RefreshCaptionButtonsWidth(e.NewSize.Width); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + RefreshCaptionButtonsWidth(Bounds.Width); + } + + void RefreshCaptionButtonsWidth(double newValue) + { + if (TopLevel.GetTopLevel(this) is Window window) + SetCaptionButtonsWidth(window, newValue); + } + } +} diff --git a/ReCap.CommonUI/Controls/Closeable.cs b/ReCap.CommonUI/Controls/Closeable.cs new file mode 100644 index 0000000..478f723 --- /dev/null +++ b/ReCap.CommonUI/Controls/Closeable.cs @@ -0,0 +1,176 @@ +using System; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace ReCap.CommonUI +{ + [TemplatePart(_PART_CloseButton, typeof(Button))] + public partial class Closeable + : ContentControl + , ICommandSource + { +#nullable enable + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(); + //AvaloniaProperty.Register(nameof(Command), enableDataValidation: true); + /// + /// Gets or sets an to be invoked when the button is clicked. + /// + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + + + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + //AvaloniaProperty.Register(nameof(CommandParameter)); + /// + /// Gets or sets a parameter to be passed to the . + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + + + + /// + /// Defines the event. + /// + public static readonly RoutedEvent CloseButtonClickEvent = + RoutedEvent.Register(nameof(CloseButtonClick), RoutingStrategies.Bubble); + /// + /// Raised when the user clicks the close button. + /// + public event EventHandler? CloseButtonClick + { + add => AddHandler(CloseButtonClickEvent, value); + remove => RemoveHandler(CloseButtonClickEvent, value); + } + + + + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen), true); + /// + /// Gets or sets a value indicating whether the + /// content area is open and visible. + /// + public bool IsOpen + { + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + + + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCloseButtonVisibleProperty = + AvaloniaProperty.Register( + nameof(IsCloseButtonVisible) + , defaultValue: true + /* + , defaultBindingMode: BindingMode.TwoWay + , coerce: CoerceIsExpanded + */ + ); + public bool IsCloseButtonVisible + { + get => GetValue(IsCloseButtonVisibleProperty); + set => SetValue(IsCloseButtonVisibleProperty, value); + } +#nullable restore + + + + + const string _PART_CloseButton = "PART_CloseButton"; + + + static Closeable() + { + //IsVisibleProperty.OverrideDefaultValue(false); + } + + + Button _closeButton = null; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (_closeButton != null) + { + _closeButton.Click -= CloseButton_Click; + //_closeButton.Click + _closeButton = null; + } + + _closeButton = e.NameScope.Get