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. ///