Skip to content

Commit

Permalink
🎨 Finally fix the ticks layout in the control curve editor.
Browse files Browse the repository at this point in the history
This was far from critical but it was utterly ugly.
There might be better solutions than the one currently impelmented, but gives the expected results, so that would be good enough.

Had to resort to Binding with TemplatedParent instead of TemplateBinding (bug?) at many places though. But again, it works…
  • Loading branch information
hexawyz committed Jan 11, 2025
1 parent 522671f commit 6b8dd8d
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 94 deletions.
1 change: 1 addition & 0 deletions src/Exo/Ui/Exo.Settings.Ui/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
<lconverters:HalfProbabilityToDoublePercentageConverter x:Key="HalfProbabilityToDoublePercentageConverter" />
<lconverters:TimeSpanToSecondsConverter x:Key="TimeSpanToSecondsConverter" />
<lconverters:SecondsToStringConverter x:Key="SecondsToStringConverter" />
<lconverters:GridLengthConverter x:Key="GridLengthConverter" />

<local:RgbLightingDefaultPalette x:Key="RgbLightingDefaultPalette" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,20 @@ public Brush LiveValueStroke
}

public static readonly DependencyProperty LiveValueStrokeProperty = RegisterProperty<Brush>(nameof(LiveValueStroke), new SolidColorBrush());

public double VerticalMargin
{
get => (double)GetValue(VerticalMarginProperty);
set => SetValue(VerticalMarginProperty, value);
}

public static readonly DependencyProperty VerticalMarginProperty = RegisterPropertyWithChangeHandler(nameof(VerticalMargin), 10d);

public double HorizontalMargin
{
get => (double)GetValue(HorizontalMarginProperty);
set => SetValue(HorizontalMarginProperty, value);
}

public static readonly DependencyProperty HorizontalMarginProperty = RegisterPropertyWithChangeHandler(nameof(HorizontalMargin), 20d);
}
91 changes: 56 additions & 35 deletions src/Exo/Ui/Exo.Settings.Ui/Controls/PowerControlCurveEditor.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<Setter Property="SymbolStroke" Value="Transparent" />
<Setter Property="SymbolStrokeThickness" Value="0" />
<Setter Property="SymbolRadius" Value="2" />
<Setter Property="HorizontalMargin" Value="20" />
<Setter Property="VerticalMargin" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PowerControlCurveEditor">
Expand All @@ -33,30 +35,66 @@
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin, Converter={StaticResource GridLengthConverter}}" MaxWidth="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin, Converter={StaticResource GridLengthConverter}}" MaxWidth="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin}" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=VerticalMargin, Converter={StaticResource GridLengthConverter}}" MaxHeight="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=VerticalMargin}" />
<RowDefinition Height="*" />
<RowDefinition Height="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=VerticalMargin, Converter={StaticResource GridLengthConverter}}" MaxHeight="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=VerticalMargin}" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsRepeater x:Name="PART_VerticalTicksItemsRepeater" Grid.Row="0" Grid.Column="0">
<ItemsRepeater.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalTextAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<local:TickLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource PercentConverter}}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<Grid Grid.Row="0" Grid.Column="1" Margin="6" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!-- Wrapping the ticks within a grid is some kind of workaround to add margin to the ticks to compensate for the margin added to the chart layout grid. -->
<Grid Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius, Converter={StaticResource GridLengthConverter}}" MaxHeight="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius}" />
<RowDefinition Height="*" />
<RowDefinition Height="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius, Converter={StaticResource GridLengthConverter}}" MaxHeight="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius}" />
</Grid.RowDefinitions>
<ItemsRepeater x:Name="PART_VerticalTicksItemsRepeater" Grid.Row="1" Margin="0,0,5,0">
<ItemsRepeater.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalTextAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<local:TickLayout Orientation="Vertical" TickHalfSize="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=VerticalMargin}" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource PercentConverter}}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
<Grid Grid.Row="2" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius, Converter={StaticResource GridLengthConverter}}" MaxWidth="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SymbolRadius, Converter={StaticResource GridLengthConverter}}" MaxWidth="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin}" />
</Grid.ColumnDefinitions>
<ItemsRepeater x:Name="PART_HorizontalTicksItemsRepeater" Grid.Column="1">
<ItemsRepeater.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<local:TickLayout Orientation="Horizontal" TickHalfSize="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=HorizontalMargin}" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource InputValueConverter}}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
<Grid Grid.Row="1" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid
Margin="{Binding SymbolRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource RadiusToThicknessConverter}}"
HorizontalAlignment="Stretch"
Expand All @@ -75,23 +113,6 @@
</ToolTipService.ToolTip>
</Path>
</Grid>
<ItemsRepeater x:Name="PART_HorizontalTicksItemsRepeater" Grid.Row="1" Grid.Column="1">
<ItemsRepeater.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<local:TickLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource InputValueConverter}}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
</ControlTemplate>
</Setter.Value>
Expand Down
115 changes: 56 additions & 59 deletions src/Exo/Ui/Exo.Settings.Ui/Controls/TickLayout.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation;

namespace Exo.Settings.Ui.Controls;
Expand All @@ -20,6 +21,20 @@ public Orientation Orientation
new PropertyMetadata(Orientation.Vertical, static (s, e) => ((TickLayout)s).OnOrientationChanged())
);

public double TickHalfSize
{
get => (double)GetValue(TickHalfSizeProperty);
set => SetValue(TickHalfSizeProperty, value);
}

public static readonly DependencyProperty TickHalfSizeProperty = DependencyProperty.Register
(
nameof(TickHalfSize),
typeof(double),
typeof(TickLayout),
new PropertyMetadata(10d)
);

public TickLayout()
{
}
Expand All @@ -30,68 +45,33 @@ protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Si
{
if (context.Children.Count == 0) return new(0, 0);

// In the first pass, all children are queried for their ideal size as if they were alone.
var tickSize = (float)(2 * TickHalfSize);

// We chose to allow overlap of children in case there would not be enough space. This is the simplest way to allocate space.
Size availableSizePerChild = Orientation == Orientation.Vertical ?
new(availableSize._width, Math.Min(availableSize._height, tickSize)) :
new(Math.Min(availableSize._width, tickSize), availableSize._height);

// Query all children for their size with one of the two dimensions already fixed.
float maxIdealWidth = 0;
float maxIdealHeight = 0;
foreach (var child in context.Children)
{
child.Measure(availableSize);
child.Measure(availableSizePerChild);
var size = child.DesiredSize;
if (float.IsFinite(size._width) && size._width > maxIdealWidth) maxIdealWidth = size._width;
if (float.IsFinite(size._height) && size._height > maxIdealHeight) maxIdealHeight = size._height;
}

// After the initial measures, we can determine the biggest ideal size, capped by availableSize.
float maxTotalWidth = maxIdealWidth;
float maxTotalHeight = maxIdealHeight;
float itemWidth = maxTotalWidth;
float itemHeight = maxTotalHeight;
if (Orientation == Orientation.Vertical)
{
if (maxTotalWidth > availableSize._width) maxTotalWidth = availableSize._width;
maxTotalHeight *= context.Children.Count;
if (maxTotalHeight > availableSize._height) maxTotalHeight = availableSize._height;

itemHeight = maxTotalHeight / context.Children.Count;
}
else
{
if (maxTotalHeight > availableSize._height) maxTotalHeight = availableSize._height;
maxTotalWidth *= context.Children.Count;
if (maxTotalWidth > availableSize._width) maxTotalWidth = availableSize._width;

itemWidth = maxTotalHeight / context.Children.Count;
}

// The second pass will determine if we can shrink the size we determined above.
// NB: We could loop and adjust many times, but we would have no guarantee that the algorithm would converge at some point.
var itemSize = new Size(itemWidth, itemHeight);
float maxIdealWidth2 = 0;
float maxIdealHeight2 = 0;
foreach (var child in context.Children)
{
child.Measure(itemSize);
var size = child.DesiredSize;
if (float.IsFinite(size._width) && size._width > maxIdealWidth2) maxIdealWidth2 = size._width;
if (float.IsFinite(size._height) && size._height > maxIdealHeight2) maxIdealHeight2 = size._height;
}

float maxTotalWidth2 = maxTotalWidth;
float maxTotalHeight2 = maxTotalHeight;
// Compute the final size using the variable dimension as we are allowed to.
if (Orientation == Orientation.Vertical)
{
if (maxIdealWidth2 < maxTotalWidth2) maxTotalWidth2 = maxIdealWidth2;
maxTotalHeight2 = maxIdealHeight2 * context.Children.Count;
if (maxTotalHeight2 > maxTotalHeight) maxTotalHeight2 = maxTotalHeight;
return new(Math.Min(availableSize._width, maxIdealWidth), Math.Min(availableSize._height, availableSizePerChild._height * context.Children.Count));
}
else
{
if (maxIdealHeight2 < maxTotalHeight2) maxTotalHeight2 = maxIdealHeight2;
maxTotalWidth2 = maxIdealWidth2 * context.Children.Count;
if (maxTotalWidth2 > maxTotalWidth) maxTotalWidth2 = maxTotalWidth;
return new(Math.Min(availableSize._width, availableSizePerChild._width * context.Children.Count), Math.Min(availableSize._height, maxIdealWidth));
}

return new(maxTotalWidth2, maxTotalHeight2);
}

protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
Expand All @@ -100,27 +80,44 @@ protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Si
if (children.Count == 0) return new Size(0, 0);
float width = float.IsFinite(finalSize._width) ? finalSize._width : 0;
float height = float.IsFinite(finalSize._height) ? finalSize._height : 0;
float size = (float)(2 * TickHalfSize);

if (Orientation == Orientation.Vertical)
{
float h = height / children.Count;

for (int i = 0; i < children.Count; i++)
float space = height - size;
if (space < 0)
{
var child = children[i];

child.Arrange(new Rect(0, i * h, width, h));
for (int i = 0; i < children.Count; i++)
{
children[i].Arrange(new Rect(0, 0, width, height));
}
}
else
{
float h = space / Math.Max(1, children.Count - 1);
for (int i = 0; i < children.Count; i++)
{
children[i].Arrange(new Rect(0, i * h, width, size));
}
}
}
else
{
float w = width / children.Count;

for (int i = 0; i < children.Count; i++)
float space = width - size;
if (space < 0)
{
var child = children[i];

child.Arrange(new Rect(i * w, 0, w, height));
for (int i = 0; i < children.Count; i++)
{
children[i].Arrange(new Rect(0, 0, width, height));
}
}
else
{
float w = space / Math.Max(1, children.Count - 1);
for (int i = 0; i < children.Count; i++)
{
children[i].Arrange(new Rect(i * w, 0, size, height));
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Converters/GridLengthConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;

namespace Exo.Settings.Ui.Converters;

internal sealed class GridLengthConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, string language) => value is not null ? new GridLength(System.Convert.ToDouble(value)) : null;

public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotSupportedException();
}

0 comments on commit 6b8dd8d

Please sign in to comment.