Skip to content

Commit a607e9d

Browse files
authored
Merge pull request #228 from reactivemarbles/AddAvaloniaControls
Add avalonia controls
2 parents c118f96 + 3e1117c commit a607e9d

74 files changed

Lines changed: 3483 additions & 41 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Collections.ObjectModel;
6+
using Avalonia;
7+
using Avalonia.Animation;
8+
using Avalonia.Animation.Easings;
9+
using Avalonia.Controls;
10+
using Avalonia.Controls.Primitives;
11+
using Avalonia.Input;
12+
using Avalonia.Media;
13+
14+
namespace CrissCross.Avalonia.UI.Controls;
15+
16+
/// <summary>
17+
/// A bottom application bar that can show and hide with animation, containing left and right content areas.
18+
/// </summary>
19+
public class AppBar : TemplatedControl
20+
{
21+
/// <summary>
22+
/// Property for <see cref="AppBarEnabled"/>.
23+
/// </summary>
24+
public static readonly StyledProperty<bool> AppBarEnabledProperty =
25+
AvaloniaProperty.Register<AppBar, bool>(nameof(AppBarEnabled), true);
26+
27+
/// <summary>
28+
/// Property for <see cref="AppBarIsSticky"/>.
29+
/// </summary>
30+
public static readonly StyledProperty<bool> AppBarIsStickyProperty =
31+
AvaloniaProperty.Register<AppBar, bool>(nameof(AppBarIsSticky));
32+
33+
/// <summary>
34+
/// Property for <see cref="AppBarLeft"/>.
35+
/// </summary>
36+
public static readonly StyledProperty<ObservableCollection<Control>> AppBarLeftProperty =
37+
AvaloniaProperty.Register<AppBar, ObservableCollection<Control>>(nameof(AppBarLeft));
38+
39+
/// <summary>
40+
/// Property for <see cref="AppBarRight"/>.
41+
/// </summary>
42+
public static readonly StyledProperty<ObservableCollection<Control>> AppBarRightProperty =
43+
AvaloniaProperty.Register<AppBar, ObservableCollection<Control>>(nameof(AppBarRight));
44+
45+
/// <summary>
46+
/// Property for <see cref="AppBarHeight"/>.
47+
/// </summary>
48+
public static readonly StyledProperty<double> AppBarHeightProperty =
49+
AvaloniaProperty.Register<AppBar, double>(nameof(AppBarHeight), 88.0);
50+
51+
/// <summary>
52+
/// Property for <see cref="IsOpen"/>.
53+
/// </summary>
54+
public static readonly StyledProperty<bool> IsOpenProperty =
55+
AvaloniaProperty.Register<AppBar, bool>(nameof(IsOpen));
56+
57+
private Border? _rootBorder;
58+
private bool _isPointerOver;
59+
60+
/// <summary>
61+
/// Initializes a new instance of the <see cref="AppBar"/> class.
62+
/// </summary>
63+
public AppBar()
64+
{
65+
AppBarLeft = [];
66+
AppBarRight = [];
67+
}
68+
69+
/// <summary>
70+
/// Gets or sets a value indicating whether the application bar is enabled.
71+
/// </summary>
72+
public bool AppBarEnabled
73+
{
74+
get => GetValue(AppBarEnabledProperty);
75+
set => SetValue(AppBarEnabledProperty, value);
76+
}
77+
78+
/// <summary>
79+
/// Gets or sets a value indicating whether the application bar stays open until explicitly closed.
80+
/// </summary>
81+
public bool AppBarIsSticky
82+
{
83+
get => GetValue(AppBarIsStickyProperty);
84+
set => SetValue(AppBarIsStickyProperty, value);
85+
}
86+
87+
/// <summary>
88+
/// Gets or sets the left-aligned content items.
89+
/// </summary>
90+
public ObservableCollection<Control> AppBarLeft
91+
{
92+
get => GetValue(AppBarLeftProperty);
93+
set => SetValue(AppBarLeftProperty, value);
94+
}
95+
96+
/// <summary>
97+
/// Gets or sets the right-aligned content items.
98+
/// </summary>
99+
public ObservableCollection<Control> AppBarRight
100+
{
101+
get => GetValue(AppBarRightProperty);
102+
set => SetValue(AppBarRightProperty, value);
103+
}
104+
105+
/// <summary>
106+
/// Gets or sets the height of the application bar when expanded.
107+
/// </summary>
108+
public double AppBarHeight
109+
{
110+
get => GetValue(AppBarHeightProperty);
111+
set => SetValue(AppBarHeightProperty, value);
112+
}
113+
114+
/// <summary>
115+
/// Gets or sets a value indicating whether the application bar is open.
116+
/// </summary>
117+
public bool IsOpen
118+
{
119+
get => GetValue(IsOpenProperty);
120+
set => SetValue(IsOpenProperty, value);
121+
}
122+
123+
/// <summary>
124+
/// Shows the application bar.
125+
/// </summary>
126+
/// <param name="isSticky">If set to <c>true</c>, the bar stays open until explicitly closed.</param>
127+
public void ShowAppBar(bool isSticky = false)
128+
{
129+
if (!AppBarEnabled)
130+
{
131+
HideAppBar();
132+
return;
133+
}
134+
135+
AppBarIsSticky = isSticky;
136+
IsOpen = true;
137+
}
138+
139+
/// <summary>
140+
/// Hides the application bar.
141+
/// </summary>
142+
public void HideAppBar() => IsOpen = false;
143+
144+
/// <inheritdoc/>
145+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
146+
{
147+
ArgumentNullException.ThrowIfNull(e);
148+
base.OnApplyTemplate(e);
149+
_rootBorder = e.NameScope.Find<Border>("PART_RootBorder");
150+
}
151+
152+
/// <inheritdoc/>
153+
protected override void OnPointerEntered(PointerEventArgs e)
154+
{
155+
base.OnPointerEntered(e);
156+
_isPointerOver = true;
157+
}
158+
159+
/// <inheritdoc/>
160+
protected override void OnPointerExited(PointerEventArgs e)
161+
{
162+
base.OnPointerExited(e);
163+
_isPointerOver = false;
164+
}
165+
166+
/// <inheritdoc/>
167+
protected override void OnPointerPressed(PointerPressedEventArgs e)
168+
{
169+
base.OnPointerPressed(e);
170+
171+
if (!_isPointerOver && IsOpen && !AppBarIsSticky)
172+
{
173+
HideAppBar();
174+
}
175+
}
176+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using Avalonia;
6+
using Avalonia.Controls.Shapes;
7+
using Avalonia.Media;
8+
9+
namespace CrissCross.Avalonia.UI.Controls;
10+
11+
/// <summary>
12+
/// Control that draws a symmetrical arc with rounded edges.
13+
/// </summary>
14+
public class Arc : Shape
15+
{
16+
/// <summary>Identifies the <see cref="StartAngle"/> dependency property.</summary>
17+
public static readonly StyledProperty<double> StartAngleProperty =
18+
AvaloniaProperty.Register<Arc, double>(nameof(StartAngle), 0.0);
19+
20+
/// <summary>Identifies the <see cref="EndAngle"/> dependency property.</summary>
21+
public static readonly StyledProperty<double> EndAngleProperty =
22+
AvaloniaProperty.Register<Arc, double>(nameof(EndAngle), 0.0);
23+
24+
/// <summary>Identifies the <see cref="SweepDirection"/> dependency property.</summary>
25+
public static readonly StyledProperty<SweepDirection> SweepDirectionProperty =
26+
AvaloniaProperty.Register<Arc, SweepDirection>(nameof(SweepDirection), SweepDirection.Clockwise);
27+
28+
static Arc()
29+
{
30+
AffectsGeometry<Arc>(StartAngleProperty, EndAngleProperty, SweepDirectionProperty);
31+
StrokeLineCapProperty.OverrideDefaultValue<Arc>(PenLineCap.Round);
32+
}
33+
34+
/// <summary>
35+
/// Gets or sets the initial angle from which the arc will be drawn.
36+
/// </summary>
37+
public double StartAngle
38+
{
39+
get => GetValue(StartAngleProperty);
40+
set => SetValue(StartAngleProperty, value);
41+
}
42+
43+
/// <summary>
44+
/// Gets or sets the final angle from which the arc will be drawn.
45+
/// </summary>
46+
public double EndAngle
47+
{
48+
get => GetValue(EndAngleProperty);
49+
set => SetValue(EndAngleProperty, value);
50+
}
51+
52+
/// <summary>
53+
/// Gets or sets the direction to where the arc will be drawn.
54+
/// </summary>
55+
public SweepDirection SweepDirection
56+
{
57+
get => GetValue(SweepDirectionProperty);
58+
set => SetValue(SweepDirectionProperty, value);
59+
}
60+
61+
/// <summary>
62+
/// Gets a value indicating whether one of the two larger arc sweeps is chosen.
63+
/// </summary>
64+
public bool IsLargeArc { get; private set; }
65+
66+
/// <inheritdoc />
67+
protected override Geometry? CreateDefiningGeometry()
68+
{
69+
IsLargeArc = Math.Abs(EndAngle - StartAngle) > 180;
70+
71+
var geometryStream = new StreamGeometry();
72+
var strokeThickness = StrokeThickness;
73+
var arcSize = new Size(
74+
Math.Max(0, (Bounds.Width - strokeThickness) / 2),
75+
Math.Max(0, (Bounds.Height - strokeThickness) / 2));
76+
77+
using (var context = geometryStream.Open())
78+
{
79+
context.BeginFigure(PointAtAngle(Math.Min(StartAngle, EndAngle)), false);
80+
81+
context.ArcTo(
82+
PointAtAngle(Math.Max(StartAngle, EndAngle)),
83+
arcSize,
84+
0,
85+
IsLargeArc,
86+
SweepDirection);
87+
}
88+
89+
geometryStream.Transform = new TranslateTransform(strokeThickness / 2, strokeThickness / 2);
90+
91+
return geometryStream;
92+
}
93+
94+
/// <summary>
95+
/// Draws a point on the coordinates of the given angle.
96+
/// </summary>
97+
/// <param name="angle">The angle at which to create the point.</param>
98+
/// <returns>A Point.</returns>
99+
private Point PointAtAngle(double angle)
100+
{
101+
var strokeThickness = StrokeThickness;
102+
103+
if (SweepDirection == SweepDirection.CounterClockwise)
104+
{
105+
angle += 90;
106+
angle %= 360;
107+
if (angle < 0)
108+
{
109+
angle += 360;
110+
}
111+
112+
var radAngle = angle * (Math.PI / 180);
113+
var xRadius = (Bounds.Width - strokeThickness) / 2;
114+
var yRadius = (Bounds.Height - strokeThickness) / 2;
115+
116+
return new Point(
117+
xRadius + (xRadius * Math.Cos(radAngle)),
118+
yRadius - (yRadius * Math.Sin(radAngle)));
119+
}
120+
else
121+
{
122+
angle -= 90;
123+
angle %= 360;
124+
if (angle < 0)
125+
{
126+
angle += 360;
127+
}
128+
129+
var radAngle = angle * (Math.PI / 180);
130+
var xRadius = (Bounds.Width - strokeThickness) / 2;
131+
var yRadius = (Bounds.Height - strokeThickness) / 2;
132+
133+
return new Point(
134+
xRadius + (xRadius * Math.Cos(-radAngle)),
135+
yRadius - (yRadius * Math.Sin(-radAngle)));
136+
}
137+
}
138+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2019-2025 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using Avalonia;
6+
using Avalonia.Media;
7+
8+
namespace CrissCross.Avalonia.UI.Controls;
9+
10+
/// <summary>
11+
/// A repeat button with a beveled/bezel appearance.
12+
/// </summary>
13+
public class BezelRepeatButton : global::Avalonia.Controls.RepeatButton
14+
{
15+
/// <summary>
16+
/// Property for <see cref="GlareOpacityMask"/>.
17+
/// </summary>
18+
public static readonly StyledProperty<IBrush?> GlareOpacityMaskProperty =
19+
AvaloniaProperty.Register<BezelRepeatButton, IBrush?>(nameof(GlareOpacityMask));
20+
21+
/// <summary>
22+
/// Property for <see cref="PressedBrush"/>.
23+
/// </summary>
24+
public static readonly StyledProperty<IBrush?> PressedBrushProperty =
25+
AvaloniaProperty.Register<BezelRepeatButton, IBrush?>(nameof(PressedBrush), Brushes.Green);
26+
27+
/// <summary>
28+
/// Property for <see cref="OuterCornerRadius"/>.
29+
/// </summary>
30+
public static readonly StyledProperty<CornerRadius> OuterCornerRadiusProperty =
31+
AvaloniaProperty.Register<BezelRepeatButton, CornerRadius>(nameof(OuterCornerRadius), new CornerRadius(3.0));
32+
33+
/// <summary>
34+
/// Property for <see cref="InnerCornerRadius"/>.
35+
/// </summary>
36+
public static readonly StyledProperty<CornerRadius> InnerCornerRadiusProperty =
37+
AvaloniaProperty.Register<BezelRepeatButton, CornerRadius>(nameof(InnerCornerRadius), new CornerRadius(2.0));
38+
39+
/// <summary>
40+
/// Gets or sets the glare opacity mask.
41+
/// </summary>
42+
public IBrush? GlareOpacityMask
43+
{
44+
get => GetValue(GlareOpacityMaskProperty);
45+
set => SetValue(GlareOpacityMaskProperty, value);
46+
}
47+
48+
/// <summary>
49+
/// Gets or sets the pressed brush.
50+
/// </summary>
51+
public IBrush? PressedBrush
52+
{
53+
get => GetValue(PressedBrushProperty);
54+
set => SetValue(PressedBrushProperty, value);
55+
}
56+
57+
/// <summary>
58+
/// Gets or sets the outer corner radius.
59+
/// </summary>
60+
public CornerRadius OuterCornerRadius
61+
{
62+
get => GetValue(OuterCornerRadiusProperty);
63+
set => SetValue(OuterCornerRadiusProperty, value);
64+
}
65+
66+
/// <summary>
67+
/// Gets or sets the inner corner radius.
68+
/// </summary>
69+
public CornerRadius InnerCornerRadius
70+
{
71+
get => GetValue(InnerCornerRadiusProperty);
72+
set => SetValue(InnerCornerRadiusProperty, value);
73+
}
74+
}

0 commit comments

Comments
 (0)