diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index f3b52428ca..85f278b5fa 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -190,6 +190,14 @@
Mica
+
+
+ LeftToRight
+ RightToLeft
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 79cf07c8d9..e8ea39abbb 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -76,6 +76,15 @@ namespace ControlCatalog
}
};
+ var flowDirections = this.Find("FlowDirection");
+ flowDirections.SelectionChanged += (sender, e) =>
+ {
+ if (flowDirections.SelectedItem is FlowDirection flowDirection)
+ {
+ this.FlowDirection = flowDirection;
+ }
+ };
+
var decorations = this.Find("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index caad8b0854..3d3ed00aac 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -13,6 +13,8 @@ namespace ControlCatalog.Pages
{
private double _leftMost;
+ protected override bool BypassFlowDirectionPolicies => true;
+
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index d4e7fc0e47..a6dee5cfaa 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -184,6 +184,25 @@ namespace Avalonia.Controls
this.UpdateSelectionBoxItem(SelectedItem);
}
+ // Because the SelectedItem isn't connected to the visual tree
+ public override void InvalidateMirrorTransform()
+ {
+ base.InvalidateMirrorTransform();
+
+ if (SelectedItem is Control selectedControl)
+ {
+ selectedControl.InvalidateMirrorTransform();
+
+ foreach (var visual in selectedControl.GetVisualDescendants())
+ {
+ if (visual is Control childControl)
+ {
+ childControl.InvalidateMirrorTransform();
+ }
+ }
+ }
+ }
+
///
protected override void OnKeyDown(KeyEventArgs e)
{
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 45c0d2948d..2c696c8f74 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -160,6 +160,16 @@ namespace Avalonia.Controls
///
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
+ ///
+ /// Gets a value indicating whether control bypass FlowDirecton policies.
+ ///
+ ///
+ /// Related to FlowDirection system and returns false as default, so if
+ /// is RTL then control will get a mirror presentation.
+ /// For controls that want to avoid this behavior, override this property and return true.
+ ///
+ protected virtual bool BypassFlowDirectionPolicies => false;
+
///
void ISetterValue.Initialize(ISetter setter)
{
@@ -219,6 +229,14 @@ namespace Avalonia.Controls
base.OnDetachedFromVisualTreeCore(e);
}
+ ///
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ InvalidateMirrorTransform();
+ }
+
///
protected override void OnGotFocus(GotFocusEventArgs e)
{
@@ -329,5 +347,55 @@ namespace Avalonia.Controls
}
}
}
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == FlowDirectionProperty)
+ {
+ InvalidateMirrorTransform();
+
+ foreach (var visual in VisualChildren)
+ {
+ if (visual is Control child)
+ {
+ child.InvalidateMirrorTransform();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Computes the value according to the
+ /// and
+ ///
+ public virtual void InvalidateMirrorTransform()
+ {
+ var flowDirection = this.FlowDirection;
+ var parentFlowDirection = FlowDirection.LeftToRight;
+
+ bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
+ bool parentBypassFlowDirectionPolicies = false;
+
+ var parent = this.FindAncestorOfType();
+ if (parent != null)
+ {
+ parentFlowDirection = parent.FlowDirection;
+ parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
+ }
+ else if (Parent is Control logicalParent)
+ {
+ parentFlowDirection = logicalParent.FlowDirection;
+ parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies;
+ }
+
+ bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
+ bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
+
+ bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
+
+ HasMirrorTransform = shouldApplyMirrorTransform;
+ }
}
}
diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs
index 3d67880638..7408bff902 100644
--- a/src/Avalonia.Controls/Image.cs
+++ b/src/Avalonia.Controls/Image.cs
@@ -66,6 +66,8 @@ namespace Avalonia.Controls
set { SetValue(StretchDirectionProperty, value); }
}
+ protected override bool BypassFlowDirectionPolicies => true;
+
///
/// Renders the control.
///
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index a6a0fc7bc5..7f2dde7c1e 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -282,6 +282,8 @@ namespace Avalonia.Controls.Presenters
}
}
+ protected override bool BypassFlowDirectionPolicies => true;
+
///
/// Creates the used to render the text.
///
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 6251d5cda7..6ac544e0fe 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -76,7 +76,7 @@ namespace Avalonia.Controls.Primitives
Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor,
- gravity, constraintAdjustment, rect);
+ gravity, constraintAdjustment, rect, FlowDirection);
UpdatePosition();
}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index 340076a407..8daf1ac68a 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -46,6 +46,7 @@ Copyright © 2019 Nikita Tsukanov
using System;
using Avalonia.VisualTree;
+using Avalonia.Media;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
@@ -444,7 +445,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
PopupAnchor anchor, PopupGravity gravity,
- PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
+ PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
+ FlowDirection flowDirection)
{
// We need a better way for tracking the last pointer position
#pragma warning disable CS0618 // Type or member is obsolete
@@ -503,6 +505,32 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
else
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
+
+ // Invert coordinate system if FlowDirection is RTL
+ if (flowDirection == FlowDirection.RightToLeft)
+ {
+ if ((positionerParameters.Anchor & PopupAnchor.Right) == PopupAnchor.Right)
+ {
+ positionerParameters.Anchor ^= PopupAnchor.Right;
+ positionerParameters.Anchor |= PopupAnchor.Left;
+ }
+ else if ((positionerParameters.Anchor & PopupAnchor.Left) == PopupAnchor.Left)
+ {
+ positionerParameters.Anchor ^= PopupAnchor.Left;
+ positionerParameters.Anchor |= PopupAnchor.Right;
+ }
+
+ if ((positionerParameters.Gravity & PopupGravity.Right) == PopupGravity.Right)
+ {
+ positionerParameters.Gravity ^= PopupGravity.Right;
+ positionerParameters.Gravity |= PopupGravity.Left;
+ }
+ else if ((positionerParameters.Gravity & PopupGravity.Left) == PopupGravity.Left)
+ {
+ positionerParameters.Gravity ^= PopupGravity.Left;
+ positionerParameters.Gravity |= PopupGravity.Right;
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 99ef93d8e8..abf56e5420 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -93,7 +93,7 @@ namespace Avalonia.Controls.Primitives
Rect? rect = null)
{
_positionerParameters.ConfigurePosition(ParentTopLevel, target,
- placement, offset, anchor, gravity, constraintAdjustment, rect);
+ placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection);
if (_positionerParameters.Size != default)
UpdatePosition();
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 8f5aa40999..703b851c79 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -318,6 +318,8 @@ namespace Avalonia.Controls
set => SetValue(TextDecorationsProperty, value);
}
+ protected override bool BypassFlowDirectionPolicies => true;
+
///
/// The BaselineOffset property provides an adjustment to baseline offset
///
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 95a597e831..55202dd20d 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -350,6 +350,12 @@ namespace Avalonia.Controls
///
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
+ public override void InvalidateMirrorTransform()
+ {
+ }
+
+ protected override bool BypassFlowDirectionPolicies => true;
+
///
/// Handles a paint notification from .
///
diff --git a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml
index fe226164a5..170ecdd5b8 100644
--- a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml
+++ b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml
@@ -26,6 +26,7 @@
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
+ FlowDirection="LeftToRight"
Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
+
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index 00246272e8..0e82ef7367 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -182,5 +182,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFo
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform.get()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 184
+Total Issues: 186
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index 23016de148..2c0298affa 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -285,6 +285,12 @@ namespace Avalonia.Rendering
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
if (clipToBounds)
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 63c22efc3f..019c3e0e9b 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -195,6 +195,12 @@ namespace Avalonia.Rendering.SceneGraph
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
using (contextImpl.BeginUpdate(node))
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 324b253a0f..26a7d5d3b8 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -68,6 +68,12 @@ namespace Avalonia
public static readonly StyledProperty OpacityMaskProperty =
AvaloniaProperty.Register(nameof(OpacityMask));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty HasMirrorTransformProperty =
+ AvaloniaProperty.RegisterDirect(nameof(HasMirrorTransform), o => o.HasMirrorTransform);
+
///
/// Defines the property.
///
@@ -96,6 +102,7 @@ namespace Avalonia
private TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot;
private IVisual? _visualParent;
+ private bool _hasMirrorTransform;
///
/// Initializes static members of the class.
@@ -107,7 +114,8 @@ namespace Avalonia
ClipProperty,
ClipToBoundsProperty,
IsVisibleProperty,
- OpacityProperty);
+ OpacityProperty,
+ HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
}
@@ -219,6 +227,15 @@ namespace Avalonia
set { SetValue(OpacityMaskProperty, value); }
}
+ ///
+ /// Gets or sets a value indicating whether to apply mirror transform on this control.
+ ///
+ public bool HasMirrorTransform
+ {
+ get { return _hasMirrorTransform; }
+ protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); }
+ }
+
///
/// Gets or sets the render transform of the control.
///
diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs
index ff8a515db3..3a3c2693d0 100644
--- a/src/Avalonia.Visuals/VisualExtensions.cs
+++ b/src/Avalonia.Visuals/VisualExtensions.cs
@@ -110,6 +110,12 @@ namespace Avalonia
result *= renderTransform;
}
+ if (v.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0);
+ result *= mirrorMatrix;
+ }
+
var topLeft = v.Bounds.TopLeft;
if (topLeft != default)
diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs
index 97c4554de6..b1251618c4 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisual.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs
@@ -75,6 +75,11 @@ namespace Avalonia.VisualTree
///
IBrush? OpacityMask { get; set; }
+ ///
+ /// Gets a value indicating whether to apply mirror transform on this control.
+ ///
+ bool HasMirrorTransform { get; }
+
///
/// Gets or sets the render transform of the control.
///