Skip to content

Commit 17ea185

Browse files
authored
enhance: image diff (#1797)
* Removed decimals from % values shown for ImageBlendSlider * Moved OLD text at ImageBlendSlider, for symmetry and non-jumpiness * Added ticks on ImageBlendSlider, at 0%, 50% and 100% * Added new image-diff mode DIFFERENCE Similar to the BLEND image-diff mode, but instead blends towards total DIFFERENCE at 50%. (This way, we can look at the difference in the middle, and slide towards OLD or NEW to view their respective contributions to the diff.) * Selected image-diff mode is now persistent
1 parent c3aea15 commit 17ea185

File tree

5 files changed

+195
-6
lines changed

5 files changed

+195
-6
lines changed

src/Converters/DoubleConverters.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ public static class DoubleConverters
1212
new FuncValueConverter<double, double>(v => v - 1.0);
1313

1414
public static readonly FuncValueConverter<double, string> ToPercentage =
15-
new FuncValueConverter<double, string>(v => (v * 100).ToString("F3") + "%");
15+
new FuncValueConverter<double, string>(v => (v * 100).ToString("F0") + "%");
1616

1717
public static readonly FuncValueConverter<double, string> OneMinusToPercentage =
18-
new FuncValueConverter<double, string>(v => ((1.0 - v) * 100).ToString("F3") + "%");
18+
new FuncValueConverter<double, string>(v => ((1.0 - v) * 100).ToString("F0") + "%");
1919

2020
public static readonly FuncValueConverter<double, Thickness> ToLeftMargin =
2121
new FuncValueConverter<double, Thickness>(v => new Thickness(v, 0, 0, 0));

src/Resources/Locales/en_US.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@
319319
<x:String x:Key="Text.Diff.First" xml:space="preserve">First Difference</x:String>
320320
<x:String x:Key="Text.Diff.IgnoreWhitespace" xml:space="preserve">Ignore All Whitespace Changes</x:String>
321321
<x:String x:Key="Text.Diff.Image.Blend" xml:space="preserve">BLEND</x:String>
322+
<x:String x:Key="Text.Diff.Image.Difference" xml:space="preserve">DIFFERENCE</x:String>
322323
<x:String x:Key="Text.Diff.Image.SideBySide" xml:space="preserve">SIDE-BY-SIDE</x:String>
323324
<x:String x:Key="Text.Diff.Image.Swipe" xml:space="preserve">SWIPE</x:String>
324325
<x:String x:Key="Text.Diff.Last" xml:space="preserve">Last Difference</x:String>

src/ViewModels/Preferences.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,12 @@ public int LFSImageActiveIdx
279279
set => SetProperty(ref _lfsImageActiveIdx, value);
280280
}
281281

282+
public int ImageDiffActiveIdx
283+
{
284+
get => _imageDiffActiveIdx;
285+
set => SetProperty(ref _imageDiffActiveIdx, value);
286+
}
287+
282288
public bool EnableCompactFoldersInChangesTree
283289
{
284290
get => _enableCompactFoldersInChangesTree;
@@ -736,6 +742,7 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
736742
private bool _showHiddenSymbolsInDiffView = false;
737743
private bool _useFullTextDiff = false;
738744
private int _lfsImageActiveIdx = 0;
745+
private int _imageDiffActiveIdx = 0;
739746
private bool _enableCompactFoldersInChangesTree = false;
740747

741748
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;

src/Views/ImageContainer.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,119 @@ private void RenderSingleSide(DrawingContext context, Bitmap img, double w, doub
361361
private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
362362
private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
363363
}
364+
365+
public class ImageDifferenceControl : ImageContainer
366+
{
367+
public static readonly StyledProperty<double> AlphaProperty =
368+
AvaloniaProperty.Register<ImageDifferenceControl, double>(nameof(Alpha), 1.0);
369+
370+
public double Alpha
371+
{
372+
get => GetValue(AlphaProperty);
373+
set => SetValue(AlphaProperty, value);
374+
}
375+
376+
public static readonly StyledProperty<Bitmap> OldImageProperty =
377+
AvaloniaProperty.Register<ImageDifferenceControl, Bitmap>(nameof(OldImage));
378+
379+
public Bitmap OldImage
380+
{
381+
get => GetValue(OldImageProperty);
382+
set => SetValue(OldImageProperty, value);
383+
}
384+
385+
public static readonly StyledProperty<Bitmap> NewImageProperty =
386+
AvaloniaProperty.Register<ImageDifferenceControl, Bitmap>(nameof(NewImage));
387+
388+
public Bitmap NewImage
389+
{
390+
get => GetValue(NewImageProperty);
391+
set => SetValue(NewImageProperty, value);
392+
}
393+
394+
static ImageDifferenceControl()
395+
{
396+
AffectsMeasure<ImageDifferenceControl>(OldImageProperty, NewImageProperty);
397+
AffectsRender<ImageDifferenceControl>(AlphaProperty);
398+
}
399+
400+
public override void Render(DrawingContext context)
401+
{
402+
base.Render(context);
403+
404+
var alpha = Alpha;
405+
var left = OldImage;
406+
var right = NewImage;
407+
var drawLeft = left != null && alpha < 1.0;
408+
var drawRight = right != null && alpha > 0.0;
409+
410+
if (drawLeft && drawRight)
411+
{
412+
using (var rt = new RenderTargetBitmap(new PixelSize((int)Bounds.Width, (int)Bounds.Height), right.Dpi))
413+
{
414+
using (var dc = rt.CreateDrawingContext())
415+
{
416+
using (dc.PushRenderOptions(RO_SRC))
417+
RenderSingleSide(dc, left, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 - 2.0 * alpha));
418+
419+
using (dc.PushRenderOptions(RO_DST))
420+
RenderSingleSide(dc, right, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 * alpha));
421+
}
422+
423+
context.DrawImage(rt, new Rect(0, 0, Bounds.Width, Bounds.Height));
424+
}
425+
}
426+
else if (drawLeft)
427+
{
428+
RenderSingleSide(context, left, Bounds.Width, Bounds.Height, 1 - alpha);
429+
}
430+
else if (drawRight)
431+
{
432+
RenderSingleSide(context, right, Bounds.Width, Bounds.Height, alpha);
433+
}
434+
}
435+
436+
protected override Size MeasureOverride(Size availableSize)
437+
{
438+
var left = OldImage;
439+
var right = NewImage;
440+
441+
if (left == null)
442+
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
443+
444+
if (right == null)
445+
return GetDesiredSize(left.Size, availableSize);
446+
447+
var ls = GetDesiredSize(left.Size, availableSize);
448+
var rs = GetDesiredSize(right.Size, availableSize);
449+
return ls.Width > rs.Width ? ls : rs;
450+
}
451+
452+
private Size GetDesiredSize(Size img, Size available)
453+
{
454+
var sw = available.Width / img.Width;
455+
var sh = available.Height / img.Height;
456+
var scale = Math.Min(1, Math.Min(sw, sh));
457+
return new Size(scale * img.Width, scale * img.Height);
458+
}
459+
460+
private void RenderSingleSide(DrawingContext context, Bitmap img, double w, double h, double alpha)
461+
{
462+
var imgW = img.Size.Width;
463+
var imgH = img.Size.Height;
464+
var scale = Math.Min(1, Math.Min(w / imgW, h / imgH));
465+
466+
var scaledW = img.Size.Width * scale;
467+
var scaledH = img.Size.Height * scale;
468+
469+
var src = new Rect(0, 0, imgW, imgH);
470+
var dst = new Rect((w - scaledW) * 0.5, (h - scaledH) * 0.5, scaledW, scaledH);
471+
472+
using (context.PushOpacity(alpha))
473+
context.DrawImage(img, src, dst);
474+
}
475+
476+
private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
477+
private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Difference, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
478+
}
364479
}

src/Views/ImageDiffView.axaml

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:m="using:SourceGit.Models"
6+
xmlns:vm="using:SourceGit.ViewModels"
67
xmlns:v="using:SourceGit.Views"
78
xmlns:c="using:SourceGit.Converters"
89
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
910
x:Class="SourceGit.Views.ImageDiffView"
1011
x:DataType="m:ImageDiff">
11-
<TabControl Margin="0,0,0,8" TabStripPlacement="Bottom">
12+
<TabControl SelectedIndex="{Binding Source={x:Static vm:Preferences.Instance}, Path=ImageDiffActiveIdx, Mode=TwoWay}" Margin="0,0,0,8" TabStripPlacement="Bottom">
1213
<TabControl.Styles>
1314
<Style Selector="TabControl /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
1415
<Setter Property="HorizontalAlignment" Value="Center"/>
@@ -129,18 +130,19 @@
129130

130131
<Grid Grid.Row="2" ColumnDefinitions="100,200,100" Margin="0,12,0,0" HorizontalAlignment="Center">
131132
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,0,8,0">
132-
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.Old}"/>
133133
<TextBlock Classes="primary"
134-
Margin="8,0,0,0"
134+
Margin="0,0,8,0"
135135
Text="{Binding #ImageBlendSlider.Value, Converter={x:Static c:DoubleConverters.OneMinusToPercentage}}"
136136
Foreground="{DynamicResource Brush.FG2}"/>
137+
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.Old}"/>
137138
</StackPanel>
138139

139140
<Slider Grid.Column="1"
140141
x:Name="ImageBlendSlider"
141142
Minimum="0" Maximum="1"
142143
VerticalAlignment="Top"
143-
TickPlacement="None"
144+
TickFrequency="0.5"
145+
TickPlacement="BottomRight"
144146
Margin="0"
145147
MinHeight="0"
146148
Foreground="{DynamicResource Brush.Border1}"
@@ -156,5 +158,69 @@
156158
</Grid>
157159
</Grid>
158160
</TabItem>
161+
162+
<TabItem>
163+
<TabItem.Header>
164+
<TextBlock Text="{DynamicResource Text.Diff.Image.Difference}" FontSize="11"/>
165+
</TabItem.Header>
166+
167+
<Grid RowDefinitions="Auto,*,Auto" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16,8,0">
168+
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" HorizontalAlignment="Center">
169+
<Border Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
170+
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.Old}" Margin="8,0" FontSize="10" Foreground="{DynamicResource Brush.BadgeFG}"/>
171+
</Border>
172+
173+
<TextBlock Grid.Column="1" Classes="primary" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
174+
<TextBlock Grid.Column="2" Classes="primary" Text="{Binding OldFileSize, Converter={x:Static c:LongConverters.ToFileSize}}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
175+
176+
<Border Grid.Column="3" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
177+
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.New}" Margin="8,0" FontSize="10" Foreground="White"/>
178+
</Border>
179+
180+
<TextBlock Grid.Column="4" Classes="primary" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
181+
<TextBlock Grid.Column="5" Classes="primary" Text="{Binding NewFileSize, Converter={x:Static c:LongConverters.ToFileSize}}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0"/>
182+
</Grid>
183+
184+
<Border Grid.Row="1" Margin="0,12,0,0" HorizontalAlignment="Center" Effect="drop-shadow(0 0 8 #A0000000)">
185+
<Border Background="{DynamicResource Brush.Window}">
186+
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
187+
<v:ImageDifferenceControl Alpha="{Binding #ImageDifferenceSlider.Value}"
188+
OldImage="{Binding Old}"
189+
NewImage="{Binding New}"/>
190+
</Border>
191+
</Border>
192+
</Border>
193+
194+
<Grid Grid.Row="2" ColumnDefinitions="100,200,100" Margin="0,12,0,0" HorizontalAlignment="Center">
195+
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,0,8,0">
196+
<TextBlock Classes="primary"
197+
Margin="0,0,8,0"
198+
Text="{Binding #ImageDifferenceSlider.Value, Converter={x:Static c:DoubleConverters.OneMinusToPercentage}}"
199+
Foreground="{DynamicResource Brush.FG2}"/>
200+
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.Old}"/>
201+
</StackPanel>
202+
203+
<Slider Grid.Column="1"
204+
x:Name="ImageDifferenceSlider"
205+
Minimum="0" Maximum="1"
206+
VerticalAlignment="Top"
207+
TickFrequency="0.5"
208+
TickPlacement="BottomRight"
209+
Margin="0"
210+
MinHeight="0"
211+
Foreground="{DynamicResource Brush.Border1}"
212+
Value="0.5"/>
213+
214+
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Top" Margin="8,0,0,0">
215+
<TextBlock Classes="primary" Text="{DynamicResource Text.Diff.New}"/>
216+
<TextBlock Classes="primary"
217+
Margin="8,0,0,0"
218+
Text="{Binding #ImageDifferenceSlider.Value, Converter={x:Static c:DoubleConverters.ToPercentage}}"
219+
Foreground="{DynamicResource Brush.FG2}"/>
220+
</StackPanel>
221+
</Grid>
222+
</Grid>
223+
</TabItem>
224+
159225
</TabControl>
160226
</UserControl>

0 commit comments

Comments
 (0)