[NUI] respect margin and padding for relative layout (#2524)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / internal / Layouting / RelativeLayout.cs
1 /* Copyright (c) 2020 Samsung Electronics Co., Ltd.
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  *
15  */
16
17 using System;
18 using System.Collections.Generic;
19 using System.Globalization;
20 using System.Linq;
21 using Tizen.NUI.BaseComponents;
22 using Tizen.NUI.Binding;
23 using Tizen.NUI.Xaml;
24
25 namespace Tizen.NUI
26 {
27     // Methods and Properties for calculation.
28     public partial class RelativeLayout
29     {
30         readonly static Dictionary<View, Relative> HorizontalRelativeCache = new Dictionary<View, Relative>();
31         readonly static Dictionary<View, Relative> VerticalRelativeCache = new Dictionary<View, Relative>();
32         private static readonly RelativePropertyGetters RelativeHorizontalPropertyGetters = new RelativePropertyGetters
33         {
34             RelativeCache = HorizontalRelativeCache,
35             GetTargetFrom = GetLeftTarget,
36             GetTargetTo = GetRightTarget,
37             GetRelativeOffsetFrom = GetLeftRelativeOffset,
38             GetRelativeOffsetTo = GetRightRelativeOffset,
39             GetAlignment = GetHorizontalAlignment,
40             GetFill = GetFillHorizontal,
41             GetMeasuredSize = (item) => item.MeasuredWidth.Size.AsDecimal() + item.Margin.Start + item.Margin.End
42         };
43         private static readonly RelativePropertyGetters RelativeVerticalPropertyGetters = new RelativePropertyGetters
44         {
45             RelativeCache = VerticalRelativeCache,
46             GetTargetFrom = GetTopTarget,
47             GetTargetTo = GetBottomTarget,
48             GetRelativeOffsetFrom = GetTopRelativeOffset,
49             GetRelativeOffsetTo = GetBottomRelativeOffset,
50             GetAlignment = GetVerticalAlignment,
51             GetFill = GetFillVertical,
52             GetMeasuredSize = (item) => item.MeasuredHeight.Size.AsDecimal() + item.Margin.Top + item.Margin.Bottom
53         };
54
55         delegate Relative GetRelative(View view, in float layoutSize);
56
57         private bool ValidateRelative(Relative relative, in float layoutSize, bool isFixedRelative)
58         {
59             float spaceSize = relative.spaceGeometry.Size;
60             float viewPosition = relative.viewGeometry.Position;
61             float viewSize = relative.viewGeometry.Size;
62             (float start, float end) space = (relative.spaceGeometry.Position, relative.spaceGeometry.Position + spaceSize);
63
64             if (spaceSize > float.Epsilon)
65             {
66                 if (!isFixedRelative && spaceSize < viewSize)
67                     return false;
68
69                 if (space.start < 0 && space.end <= layoutSize)
70                 {
71                     if (viewPosition + viewSize > layoutSize)
72                         return false;
73                 }
74                 else if (0 <= space.start && layoutSize < space.end)
75                 {
76                     if (viewPosition < 0)
77                         return false;
78                 }
79                 else if (viewPosition < 0 || viewPosition + viewSize > layoutSize)
80                 {
81                     return false;
82                 }
83             }
84             else if (space.start == 0.0)
85             {
86                 if (viewPosition + viewSize > layoutSize)
87                     return false;
88             }
89             else if (space.start == layoutSize)
90             {
91                 if (viewPosition < 0)
92                     return false;
93             }
94             else if (viewPosition < 0 || viewPosition + viewSize > layoutSize)
95             {
96                 return false;
97             }
98             return true;
99         }
100
101         private bool ValidateRelatives(in float layoutSize, HashSet<View> fixedRelativeSet, GetRelative GetRelative)
102         {
103             foreach (LayoutItem childLayout in LayoutChildren.Where(item => item?.Owner != null))
104             {
105                 Relative relative = GetRelative(childLayout.Owner, layoutSize);
106                 if (!ValidateRelative(relative, layoutSize, fixedRelativeSet.Contains(childLayout.Owner)))
107                     return false;
108             }
109
110             return true;
111         }
112
113         private HashSet<View> GetFixedRelativeSet(in float layoutSize, GetRelative GetRelative)
114         {
115             HashSet<View> ignoreSet = new HashSet<View>();
116
117             foreach (LayoutItem childLayout in LayoutChildren.Where(item => item?.Owner != null))
118             {
119                 Relative relative = GetRelative(childLayout.Owner, layoutSize);
120                 if (relative.spaceGeometry.Size > float.Epsilon
121                     && relative.spaceGeometry.Size < relative.viewGeometry.Size)
122                 {
123                     ignoreSet.Add(childLayout.Owner);
124                 }
125             }
126
127             return ignoreSet;
128         }
129
130         private Relative GetHorizontalRelative(View view, in float layoutSize) => CalculateRelative(view, layoutSize, 0, RelativeHorizontalPropertyGetters);
131
132         private Relative GetVerticalRelative(View view, in float layoutSize) => CalculateRelative(view, layoutSize, 0, RelativeVerticalPropertyGetters);
133
134         private Relative CalculateRelative(View view, in float layoutSize, int depth, RelativePropertyGetters propertyGetters)
135         {
136             if (propertyGetters.RelativeCache.TryGetValue(view, out Relative cache))
137                 return cache;
138
139             depth++;
140             if (depth > LayoutChildren.Count)
141             {
142                 throw new InvalidOperationException("Circular dependency detected in RelativeLayout.");
143             }
144             Relative cacheFrom;
145             Relative cacheTo;
146             float RelativeOffsetFrom = propertyGetters.GetRelativeOffsetFrom(view);
147             float RelativeOffsetTo = propertyGetters.GetRelativeOffsetTo(view);
148             float viewSize = propertyGetters.GetMeasuredSize(view.Layout);
149
150             if (propertyGetters.GetTargetFrom(view) is View targetFrom && targetFrom != Owner)
151             {
152                 cacheFrom = CalculateRelative(targetFrom, layoutSize, depth, propertyGetters);
153             }
154             else
155             {
156                 cacheFrom = new Relative() { viewGeometry = new Geometry(0, layoutSize) };
157             }
158
159             if (propertyGetters.GetTargetTo(view) is View targetTo && targetTo != Owner)
160             {
161                 cacheTo = CalculateRelative(targetTo, layoutSize, depth, propertyGetters);
162             }
163             else
164             {
165                 cacheTo = new Relative() { viewGeometry = new Geometry(0, layoutSize) };
166             }
167
168             float startPosition = cacheFrom.viewGeometry.Position + cacheFrom.viewGeometry.Size * RelativeOffsetFrom;
169             float endPosition = cacheTo.viewGeometry.Position + cacheTo.viewGeometry.Size * RelativeOffsetTo;
170             if (startPosition > endPosition)
171                 (startPosition, endPosition) = (endPosition, startPosition);
172
173             float alignment = propertyGetters.GetAlignment(view).ToFloat();
174             bool fill = propertyGetters.GetFill(view);
175
176             Geometry viewGeometry = new Geometry(startPosition + ((endPosition - startPosition - viewSize) * alignment), viewSize);
177             Geometry spaceGeometry = new Geometry(startPosition, Math.Abs(endPosition - startPosition));
178             if (fill && spaceGeometry.Size > viewGeometry.Size)
179             {
180                 viewGeometry = spaceGeometry;
181             }
182
183             Relative retCache = new Relative
184             {
185                 viewGeometry = viewGeometry,
186                 spaceGeometry = spaceGeometry
187             };
188             propertyGetters.RelativeCache.Add(view, retCache);
189
190             return retCache;
191         }
192
193         private (float, float) CalculateChildrenSize(float parentWidth, float parentHeight)
194         {
195             int horizontalPadding = Padding.Start + Padding.End;
196             int verticalPadding = Padding.Top + Padding.Bottom;
197             int minWidth = (int)SuggestedMinimumWidth.AsDecimal() - horizontalPadding;
198             int maxWidth = (int)parentWidth - verticalPadding;
199             int minHeight = (int)SuggestedMinimumHeight.AsDecimal() - verticalPadding;
200             int maxHeight = (int)parentHeight - verticalPadding;
201
202             if (minWidth < 0) minWidth = 0;
203             if (maxWidth < 0) maxWidth = 0;
204             if (minHeight < 0) minHeight = 0;
205             if (maxHeight < 0) maxHeight = 0;
206
207             // Find minimum size that satisfy all constraints
208             float BinarySearch(int min, int max, Dictionary<View, Relative> Cache, GetRelative GetRelative)
209             {
210                 int current = (min != 0) ? min : (min + max) / 2;
211
212                 HashSet<View> fixedRelativeSet = GetFixedRelativeSet(max, GetRelative);
213                 Cache.Clear();
214                 do
215                 {
216                     if (ValidateRelatives(current, fixedRelativeSet, GetRelative))
217                     {
218                         max = current;
219                     }
220                     else
221                     {
222                         min = current + 1;
223                     }
224                     current = (min + max) / 2;
225                     Cache.Clear();
226                 } while (min < max);
227
228                 return current;
229             }
230
231             float ChildrenWidth = BinarySearch(minWidth, maxWidth, HorizontalRelativeCache, GetHorizontalRelative) + horizontalPadding;
232             float ChildrenHeight = BinarySearch(minHeight, maxHeight, VerticalRelativeCache, GetVerticalRelative) + verticalPadding;
233             return (ChildrenWidth, ChildrenHeight);
234         }
235
236         private Geometry GetHorizontalLayout(View view) => GetHorizontalRelative(view, MeasuredWidth.Size.AsDecimal() - (Padding.Start + Padding.End)).viewGeometry;
237
238         private Geometry GetVerticalLayout(View view) => GetVerticalRelative(view, MeasuredHeight.Size.AsDecimal() - (Padding.Top + Padding.Bottom)).viewGeometry;
239
240         private struct Geometry
241         {
242             public float Position { get; }
243             public float Size { get; }
244
245             public Geometry(float position, float size)
246             {
247                 Position = position;
248                 Size = size;
249             }
250         }
251
252         private struct Relative
253         {
254             public Geometry viewGeometry;
255             public Geometry spaceGeometry;
256         }
257         private struct RelativePropertyGetters
258         {
259             public Dictionary<View, Relative> RelativeCache;
260             public Func<BindableObject, View> GetTargetFrom;
261             public Func<BindableObject, View> GetTargetTo;
262             public Func<View, float> GetRelativeOffsetFrom;
263             public Func<View, float> GetRelativeOffsetTo;
264             public Func<View, Alignment> GetAlignment;
265             public Func<View, bool> GetFill;
266             public Func<LayoutItem, float> GetMeasuredSize;
267         }
268
269         class RelativeTargetConverter : TypeConverter, IExtendedTypeConverter
270         {
271             object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
272             {
273                 if (serviceProvider == null)
274                     throw new ArgumentNullException(nameof(serviceProvider));
275
276                 object target = null;
277
278                 if (serviceProvider.GetService(typeof(IReferenceProvider)) is IReferenceProvider referenceProvider)
279                     target = referenceProvider.FindByName(value);
280
281                 return target ?? throw new ArgumentException($"Can't resolve name '{value}' on Element", nameof(value));
282             }
283
284             public override object ConvertFromInvariantString(string value) => throw new NotImplementedException();
285
286             public object ConvertFrom(CultureInfo culture, object value, IServiceProvider serviceProvider) => throw new NotImplementedException();
287         }
288     }
289 }