Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,12 +661,18 @@ This section provides an overview of all available classes and their purpose in
*Automatically selects a ListBoxItem when the pointer moves over it.*

### Responsive
- **AdaptiveBehavior**
- **AdaptiveBehavior**
*Observes bounds changes of a control (or a specified source) and conditionally adds or removes CSS-style classes based on adaptive rules.*

- **AdaptiveClassSetter**
- **AdaptiveClassSetter**
*Specifies comparison conditions (min/max width/height) and the class to apply when those conditions are met.*

- **AspectRatioBehavior**
*Observes bounds changes and toggles CSS-style classes when the control's aspect ratio matches specified rules.*

- **AspectRatioClassSetter**
*Defines aspect ratio comparison conditions and the class to apply when those conditions are met.*

### ScrollViewer
- **HorizontalScrollViewerBehavior**
*Enables horizontal scrolling via the pointer wheel. Optionally requires the Shift key and supports line or page scrolling.*
Expand Down
3 changes: 3 additions & 0 deletions samples/BehaviorsTestApplication/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
<TabItem Header="AdaptiveBehavior">
<pages:AdaptiveBehaviorView />
</TabItem>
<TabItem Header="AspectRatioBehavior">
<pages:AspectRatioBehaviorView />
</TabItem>
<TabItem Header="EditableListBox">
<pages:EditableListBoxView />
</TabItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<UserControl x:Class="BehaviorsTestApplication.Views.Pages.AspectRatioBehaviorView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:BehaviorsTestApplication.ViewModels"
x:DataType="vm:MainWindowViewModel"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>

<UserControl.Styles>
<Style Selector="StackPanel.responsive">
<Setter Property="(Interaction.Behaviors)">
<BehaviorCollectionTemplate>
<BehaviorCollection>
<AspectRatioBehavior SourceControl="{Binding $parent[TopLevel]}">
<AspectRatioClassSetter MinRatio="0" MaxRatio="1" ClassName="vertical" />
<AspectRatioClassSetter MinRatio="1" MaxRatio="Infinity" ClassName="horizontal" />
</AspectRatioBehavior>
</BehaviorCollection>
</BehaviorCollectionTemplate>
</Setter>
</Style>
<Style Selector="StackPanel.horizontal">
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="StackPanel.vertical">
<Setter Property="Orientation" Value="Vertical" />
</Style>
</UserControl.Styles>

<Grid Background="{DynamicResource GrayBrush}" Margin="5">
<DockPanel>
<StackPanel Orientation="Horizontal" Spacing="5" Margin="5" DockPanel.Dock="Bottom">
<TextBlock Text="Width: " />
<TextBlock Text="{Binding $parent[TopLevel].Bounds.Width}" />
<TextBlock Text=" Height: " />
<TextBlock Text="{Binding $parent[TopLevel].Bounds.Height}" />
<TextBlock Text=" Classes: " />
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{ClassesToStringConverter}">
<Binding ElementName="ResponsiveStackPanel" Path="Classes.Count" />
<Binding ElementName="ResponsiveStackPanel" Path="Classes" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<StackPanel Classes="responsive" Name="ResponsiveStackPanel" Margin="5">
<TextBlock Text="Item1" />
<TextBlock Text="Item2" />
<TextBlock Text="Item3" />
<TextBlock Text="Item4" />
</StackPanel>
</DockPanel>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace BehaviorsTestApplication.Views.Pages;

public partial class AspectRatioBehaviorView : UserControl
{
public AspectRatioBehaviorView()
{
InitializeComponent();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
193 changes: 193 additions & 0 deletions src/Avalonia.Xaml.Interactions.Responsive/AspectRatioBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Xaml.Interactivity;

namespace Avalonia.Xaml.Interactions.Responsive;

/// <summary>
/// Observes bounds changes of a control (or a specified source) and conditionally adds or removes classes
/// based on <see cref="AspectRatioClassSetter"/> rules.
/// </summary>
public class AspectRatioBehavior : StyledElementBehavior<Control>
{
private IDisposable? _disposable;
private AvaloniaList<AspectRatioClassSetter>? _setters;

/// <summary>
/// Identifies the <seealso cref="SourceControl"/> avalonia property.
/// </summary>
public static readonly StyledProperty<Control?> SourceControlProperty =
AvaloniaProperty.Register<AspectRatioBehavior, Control?>(nameof(SourceControl));

/// <summary>
/// Identifies the <seealso cref="TargetControl"/> avalonia property.
/// </summary>
public static readonly StyledProperty<Control?> TargetControlProperty =
AvaloniaProperty.Register<AspectRatioBehavior, Control?>(nameof(TargetControl));

/// <summary>
/// Identifies the <seealso cref="Setters"/> avalonia property.
/// </summary>
public static readonly DirectProperty<AspectRatioBehavior, AvaloniaList<AspectRatioClassSetter>> SettersProperty =
AvaloniaProperty.RegisterDirect<AspectRatioBehavior, AvaloniaList<AspectRatioClassSetter>>(nameof(Setters), t => t.Setters);

/// <summary>
/// Gets or sets the control whose bounds are observed. If not set, <see cref="StyledElementBehavior{T}.AssociatedObject"/> is used. This is an avalonia property.
/// </summary>
[ResolveByName]
public Control? SourceControl
{
get => GetValue(SourceControlProperty);
set => SetValue(SourceControlProperty, value);
}

/// <summary>
/// Gets or sets the target control to add or remove classes from. If not set, the associated object or setter TargetControl is used. This is an avalonia property.
/// </summary>
[ResolveByName]
public Control? TargetControl
{
get => GetValue(TargetControlProperty);
set => SetValue(TargetControlProperty, value);
}

/// <summary>
/// Gets aspect ratio class setters collection. This is an avalonia property.
/// </summary>
[Content]
public AvaloniaList<AspectRatioClassSetter> Setters => _setters ??= [];

/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
base.OnAttachedToVisualTree();
StopObserving();
StartObserving();
}

/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
base.OnDetachedFromVisualTree();
StopObserving();
}

private void StartObserving()
{
var sourceControl = GetValue(SourceControlProperty) is not null
? SourceControl
: AssociatedObject;

if (sourceControl is not null)
{
_disposable = ObserveBounds(sourceControl);
}
}

private void StopObserving()
{
_disposable?.Dispose();
}

private IDisposable ObserveBounds(Control sourceControl)
{
if (sourceControl is null)
{
throw new ArgumentNullException(nameof(sourceControl));
}

Execute(sourceControl, Setters, sourceControl.GetValue(Visual.BoundsProperty));

return sourceControl.GetObservable(Visual.BoundsProperty)
.Subscribe(new AnonymousObserver<Rect>(bounds => Execute(sourceControl, Setters, bounds)));
}

private void Execute(Control? sourceControl, AvaloniaList<AspectRatioClassSetter>? setters, Rect bounds)
{
if (sourceControl is null || setters is null)
{
return;
}

var ratio = bounds.Height > 0 ? bounds.Width / bounds.Height : double.PositiveInfinity;

foreach (var setter in setters)
{
var ratioConditionTriggered = GetResult(setter.MinRatioOperator, ratio, setter.MinRatio) &&
GetResult(setter.MaxRatioOperator, ratio, setter.MaxRatio);

var targetControl = setter.GetValue(AspectRatioClassSetter.TargetControlProperty) is not null
? setter.TargetControl
: GetValue(TargetControlProperty) is not null
? TargetControl
: AssociatedObject;

if (targetControl is not null)
{
var className = setter.ClassName;
var isPseudoClass = setter.IsPseudoClass;

if (ratioConditionTriggered)
{
Add(targetControl, className, isPseudoClass);
}
else
{
Remove(targetControl, className, isPseudoClass);
}
}
else
{
throw new ArgumentNullException(nameof(targetControl));
}
}
}

private static bool GetResult(ComparisonConditionType comparisonConditionType, double property, double value) => comparisonConditionType switch
{
ComparisonConditionType.Equal => property == value,
ComparisonConditionType.NotEqual => property != value,
ComparisonConditionType.LessThan => property < value,
ComparisonConditionType.LessThanOrEqual => property <= value,
ComparisonConditionType.GreaterThan => property > value,
ComparisonConditionType.GreaterThanOrEqual => property >= value,
_ => throw new ArgumentOutOfRangeException()
};

private static void Add(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || targetControl.Classes.Contains(className))
{
return;
}

if (isPseudoClass)
{
((IPseudoClasses)targetControl.Classes).Add(className);
}
else
{
targetControl.Classes.Add(className);
}
}

private static void Remove(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || !targetControl.Classes.Contains(className))
{
return;
}

if (isPseudoClass)
{
((IPseudoClasses)targetControl.Classes).Remove(className);
}
else
{
targetControl.Classes.Remove(className);
}
}
}
Loading
Loading