mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-10-31 13:03:20 -07:00
feature: add two view mode for image diff - side-by-side and blend
This commit is contained in:
parent
d3d6889e25
commit
8e88df92b3
10 changed files with 490 additions and 231 deletions
19
src/Converters/DoubleConverters.cs
Normal file
19
src/Converters/DoubleConverters.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace SourceGit.Converters
|
||||
{
|
||||
public static class DoubleConverters
|
||||
{
|
||||
public static readonly FuncValueConverter<double, double> Increase =
|
||||
new FuncValueConverter<double, double>(v => v + 1.0);
|
||||
|
||||
public static readonly FuncValueConverter<double, double> Decrease =
|
||||
new FuncValueConverter<double, double>(v => v - 1.0);
|
||||
|
||||
public static readonly FuncValueConverter<double, string> ToPercentage =
|
||||
new FuncValueConverter<double, string>(v => (v * 100).ToString("F3") + "%");
|
||||
|
||||
public static readonly FuncValueConverter<double, string> OneMinusToPercentage =
|
||||
new FuncValueConverter<double, string>(v => ((1.0 - v) * 100).ToString("F3") + "%");
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace SourceGit.Converters
|
||||
{
|
||||
public static class FontSizeModifyConverters
|
||||
{
|
||||
public static readonly FuncValueConverter<double, double> Increase =
|
||||
new FuncValueConverter<double, double>(v => v + 1.0);
|
||||
|
||||
public static readonly FuncValueConverter<double, double> Decrease =
|
||||
new FuncValueConverter<double, double>(v => v - 1.0);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@
|
|||
</DockPanel>
|
||||
<SelectableTextBlock Text="{Binding AuthorTimeStr}"
|
||||
Margin="2,0,0,0"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Decrease}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
|
||||
Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
@ -41,7 +41,7 @@
|
|||
</DockPanel>
|
||||
<SelectableTextBlock Text="{Binding CommitterTimeStr}"
|
||||
Margin="2,0,0,0"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Decrease}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
|
||||
Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
|
|
@ -184,60 +184,7 @@
|
|||
|
||||
<!-- Image Diff -->
|
||||
<DataTemplate DataType="m:ImageDiff">
|
||||
<Grid RowDefinitions="Auto,*" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16,8,0">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" HorizontalAlignment="Center">
|
||||
<Border Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
|
||||
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="5" Classes="monospace" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="6" Classes="monospace" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="7" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" RowDefinitions="*,Auto,Auto" Margin="0,16,0,0" HorizontalAlignment="Center">
|
||||
<Border Grid.Row="0" Background="{DynamicResource Brush.Window}" Effect="drop-shadow(0 0 8 #A0000000)">
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
|
||||
<v:ImageDiffView Alpha="{Binding #ImageDiffSlider.Value}"
|
||||
OldImage="{Binding Old}"
|
||||
NewImage="{Binding New}"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,*" Margin="0,8,0,0">
|
||||
<TextBlock Grid.Column="0" Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Foreground="{DynamicResource Brush.FG2}" FontSize="10"/>
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Right" FontSize="10"/>
|
||||
</Grid>
|
||||
|
||||
<Slider Grid.Row="2"
|
||||
x:Name="ImageDiffSlider"
|
||||
Minimum="0" Maximum="1"
|
||||
VerticalAlignment="Top"
|
||||
TickPlacement="None"
|
||||
Margin="8,4,8,0"
|
||||
MinHeight="0"
|
||||
Foreground="{DynamicResource Brush.Border1}"
|
||||
Value="0.5">
|
||||
<Slider.Resources>
|
||||
<Thickness x:Key="SliderTopHeaderMargin">0,0,0,4</Thickness>
|
||||
<GridLength x:Key="SliderPreContentMargin">0</GridLength>
|
||||
<GridLength x:Key="SliderPostContentMargin">0</GridLength>
|
||||
<CornerRadius x:Key="SliderThumbCornerRadius">8</CornerRadius>
|
||||
<x:Double x:Key="SliderHorizontalThumbWidth">16</x:Double>
|
||||
<x:Double x:Key="SliderHorizontalThumbHeight">16</x:Double>
|
||||
</Slider.Resources>
|
||||
</Slider>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<v:ImageDiffView/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Text Diff -->
|
||||
|
|
|
@ -1,164 +1,7 @@
|
|||
using System;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace SourceGit.Views
|
||||
{
|
||||
public class ImageDiffView : Control
|
||||
{
|
||||
public static readonly StyledProperty<double> AlphaProperty =
|
||||
AvaloniaProperty.Register<ImageDiffView, double>(nameof(Alpha), 0.5);
|
||||
|
||||
public double Alpha
|
||||
{
|
||||
get => GetValue(AlphaProperty);
|
||||
set => SetValue(AlphaProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> OldImageProperty =
|
||||
AvaloniaProperty.Register<ImageDiffView, Bitmap>(nameof(OldImage), null);
|
||||
|
||||
public Bitmap OldImage
|
||||
{
|
||||
get => GetValue(OldImageProperty);
|
||||
set => SetValue(OldImageProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> NewImageProperty =
|
||||
AvaloniaProperty.Register<ImageDiffView, Bitmap>(nameof(NewImage), null);
|
||||
|
||||
public Bitmap NewImage
|
||||
{
|
||||
get => GetValue(NewImageProperty);
|
||||
set => SetValue(NewImageProperty, value);
|
||||
}
|
||||
|
||||
static ImageDiffView()
|
||||
{
|
||||
AffectsMeasure<ImageDiffView>(OldImageProperty, NewImageProperty);
|
||||
AffectsRender<ImageDiffView>(AlphaProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
if (_bgBrush == null)
|
||||
{
|
||||
var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB);
|
||||
var bg = new DrawingGroup()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) },
|
||||
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) },
|
||||
}
|
||||
};
|
||||
|
||||
_bgBrush = new DrawingBrush(bg)
|
||||
{
|
||||
AlignmentX = AlignmentX.Left,
|
||||
AlignmentY = AlignmentY.Top,
|
||||
DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute),
|
||||
Stretch = Stretch.None,
|
||||
TileMode = TileMode.Tile,
|
||||
};
|
||||
}
|
||||
|
||||
context.FillRectangle(_bgBrush, new Rect(Bounds.Size));
|
||||
|
||||
var alpha = Alpha;
|
||||
var w = Bounds.Width - 16;
|
||||
var h = Bounds.Height - 16;
|
||||
var x = w * alpha;
|
||||
var left = OldImage;
|
||||
if (left != null && alpha > 0)
|
||||
{
|
||||
var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height);
|
||||
var dst = new Rect(8, 8, x, h);
|
||||
context.DrawImage(left, src, dst);
|
||||
}
|
||||
|
||||
var right = NewImage;
|
||||
if (right != null && alpha < 1)
|
||||
{
|
||||
var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height);
|
||||
var dst = new Rect(x + 8, 8, w - x, h);
|
||||
context.DrawImage(right, src, dst);
|
||||
}
|
||||
|
||||
context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x + 8, 0), new Point(x + 8, Bounds.Height));
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property.Name == "ActualThemeVariant")
|
||||
{
|
||||
_bgBrush = null;
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var left = OldImage;
|
||||
var right = NewImage;
|
||||
|
||||
if (left != null)
|
||||
{
|
||||
var lSize = GetDesiredSize(left.Size, availableSize);
|
||||
if (right != null)
|
||||
{
|
||||
var rSize = GetDesiredSize(right.Size, availableSize);
|
||||
if (rSize.Width > lSize.Width)
|
||||
return rSize;
|
||||
return lSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return lSize;
|
||||
}
|
||||
}
|
||||
else if (right != null)
|
||||
{
|
||||
return GetDesiredSize(right.Size, availableSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
return availableSize;
|
||||
}
|
||||
}
|
||||
|
||||
private Size GetDesiredSize(Size img, Size available)
|
||||
{
|
||||
var w = available.Width - 16;
|
||||
var h = available.Height - 16;
|
||||
|
||||
if (img.Width <= w)
|
||||
{
|
||||
if (img.Height <= h)
|
||||
{
|
||||
return new Size(img.Width + 16, img.Height + 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Size(h * img.Width / img.Height + 16, available.Height);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var s = Math.Max(img.Width / w, img.Height / h);
|
||||
return new Size(img.Width / s + 16, img.Height / s + 16);
|
||||
}
|
||||
}
|
||||
|
||||
private DrawingBrush _bgBrush = null;
|
||||
}
|
||||
|
||||
public partial class DiffView : UserControl
|
||||
{
|
||||
public DiffView()
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<TextBlock Text="{DynamicResource Text.Hotkeys.Global}"
|
||||
Foreground="{DynamicResource Brush.FG2}"
|
||||
FontWeight="Bold"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Increase}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<Grid RowDefinitions="20,20,20,20,20,20" ColumnDefinitions="150,*">
|
||||
|
@ -81,7 +81,7 @@
|
|||
<TextBlock Text="{DynamicResource Text.Hotkeys.Repo}"
|
||||
Foreground="{DynamicResource Brush.FG2}"
|
||||
FontWeight="Bold"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Increase}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}"
|
||||
Margin="0,8"/>
|
||||
|
||||
<Grid RowDefinitions="20,20,20,20,20,20,20,20" ColumnDefinitions="150,*">
|
||||
|
@ -113,7 +113,7 @@
|
|||
<TextBlock Text="{DynamicResource Text.Hotkeys.TextEditor}"
|
||||
Foreground="{DynamicResource Brush.FG2}"
|
||||
FontWeight="Bold"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Increase}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}"
|
||||
Margin="0,8"/>
|
||||
|
||||
<Grid RowDefinitions="20,20,20,20" ColumnDefinitions="150,*">
|
||||
|
|
169
src/Views/ImageDiffView.axaml
Normal file
169
src/Views/ImageDiffView.axaml
Normal file
|
@ -0,0 +1,169 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:m="using:SourceGit.Models"
|
||||
xmlns:vm="using:SourceGit.ViewModels"
|
||||
xmlns:v="using:SourceGit.Views"
|
||||
xmlns:c="using:SourceGit.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="SourceGit.Views.ImageDiffView"
|
||||
x:DataType="m:ImageDiff">
|
||||
<TabControl Margin="0,0,0,8" TabStripPlacement="Bottom">
|
||||
<TabControl.Styles>
|
||||
<Style Selector="TabControl /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</TabControl.Styles>
|
||||
|
||||
<TabItem Header="SIDE-BY-SIDE">
|
||||
<Grid ColumnDefinitions="*,32,*" RowDefinitions="Auto,*" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16">
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Border Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" Grid.Column="0" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
|
||||
<Border Background="{DynamicResource Brush.Popup}" HorizontalAlignment="Center" Padding="8">
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
|
||||
<v:ImageContainer>
|
||||
<Image Source="{Binding Old}" Stretch="Uniform" VerticalAlignment="Center"/>
|
||||
</v:ImageContainer>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Border Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" Grid.Column="2" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
|
||||
<Border Background="{DynamicResource Brush.Popup}" HorizontalAlignment="Center" Padding="8">
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}">
|
||||
<v:ImageContainer>
|
||||
<Image Source="{Binding New}" Stretch="Uniform"/>
|
||||
</v:ImageContainer>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="SWIPE">
|
||||
<Grid RowDefinitions="Auto,*" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" HorizontalAlignment="Center">
|
||||
<Border Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
|
||||
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="5" Classes="monospace" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="6" Classes="monospace" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="7" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
|
||||
<Border HorizontalAlignment="Center" Background="{DynamicResource Brush.Window}">
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
|
||||
<v:ImageContainer>
|
||||
<v:ImagesSwipeControl OldImage="{Binding Old}"
|
||||
NewImage="{Binding New}"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
||||
</v:ImageContainer>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="BLEND">
|
||||
<Grid RowDefinitions="Auto,*,Auto" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,16,8,0">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" HorizontalAlignment="Center">
|
||||
<Border Grid.Column="0" Height="16" Background="{DynamicResource Brush.Badge}" CornerRadius="8" VerticalAlignment="Center">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding OldImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding OldFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
|
||||
<Border Grid.Column="4" Height="16" Background="Green" CornerRadius="8" VerticalAlignment="Center" Margin="32,0,0,0">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}" Margin="8,0" FontSize="10"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="5" Classes="monospace" Text="{Binding NewImageSize}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="6" Classes="monospace" Text="{Binding NewFileSize}" Foreground="{DynamicResource Brush.FG2}" Margin="16,0,0,0" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="7" Classes="monospace" Text="{DynamicResource Text.Bytes}" Foreground="{DynamicResource Brush.FG2}" Margin="2,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1" Margin="0,12,0,0" Effect="drop-shadow(0 0 8 #A0000000)">
|
||||
<Border HorizontalAlignment="Center" Background="{DynamicResource Brush.Window}">
|
||||
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border1}" Margin="8">
|
||||
<v:ImageContainer>
|
||||
<v:ImageBlendControl Alpha="{Binding #ImageBlendSlider.Value}"
|
||||
OldImage="{Binding Old}"
|
||||
NewImage="{Binding New}"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
||||
</v:ImageContainer>
|
||||
</Border>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="100,200,100" Margin="0,12,0,0" HorizontalAlignment="Center">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,0,8,0">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.Old}"/>
|
||||
<TextBlock Classes="monospace"
|
||||
Margin="8,0,0,0"
|
||||
Text="{Binding #ImageBlendSlider.Value, Converter={x:Static c:DoubleConverters.ToPercentage}}"
|
||||
Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
x:Name="ImageBlendSlider"
|
||||
Minimum="0" Maximum="1"
|
||||
VerticalAlignment="Top"
|
||||
TickPlacement="None"
|
||||
Margin="0"
|
||||
MinHeight="0"
|
||||
Foreground="{DynamicResource Brush.Border1}"
|
||||
Value="0.5">
|
||||
<Slider.Resources>
|
||||
<Thickness x:Key="SliderTopHeaderMargin">0,0,0,4</Thickness>
|
||||
<GridLength x:Key="SliderPreContentMargin">0</GridLength>
|
||||
<GridLength x:Key="SliderPostContentMargin">0</GridLength>
|
||||
<CornerRadius x:Key="SliderThumbCornerRadius">8</CornerRadius>
|
||||
<x:Double x:Key="SliderHorizontalThumbWidth">16</x:Double>
|
||||
<x:Double x:Key="SliderHorizontalThumbHeight">16</x:Double>
|
||||
</Slider.Resources>
|
||||
</Slider>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Top" Margin="8,0,0,0">
|
||||
<TextBlock Classes="monospace" Text="{DynamicResource Text.Diff.Binary.New}"/>
|
||||
<TextBlock Classes="monospace"
|
||||
Margin="8,0,0,0"
|
||||
Text="{Binding #ImageBlendSlider.Value, Converter={x:Static c:DoubleConverters.OneMinusToPercentage}}"
|
||||
Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</UserControl>
|
294
src/Views/ImageDiffView.axaml.cs
Normal file
294
src/Views/ImageDiffView.axaml.cs
Normal file
|
@ -0,0 +1,294 @@
|
|||
using System;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace SourceGit.Views
|
||||
{
|
||||
public class ImageContainer : ContentControl
|
||||
{
|
||||
protected override Type StyleKeyOverride => typeof(ContentControl);
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
if (_bgBrush == null)
|
||||
{
|
||||
var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB);
|
||||
var bg = new DrawingGroup()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) },
|
||||
new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) },
|
||||
}
|
||||
};
|
||||
|
||||
_bgBrush = new DrawingBrush(bg)
|
||||
{
|
||||
AlignmentX = AlignmentX.Left,
|
||||
AlignmentY = AlignmentY.Top,
|
||||
DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute),
|
||||
Stretch = Stretch.None,
|
||||
TileMode = TileMode.Tile,
|
||||
};
|
||||
}
|
||||
|
||||
context.FillRectangle(_bgBrush, new Rect(Bounds.Size));
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property.Name == "ActualThemeVariant")
|
||||
{
|
||||
_bgBrush = null;
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
private DrawingBrush _bgBrush = null;
|
||||
}
|
||||
|
||||
public class ImagesSwipeControl : Control
|
||||
{
|
||||
public static readonly StyledProperty<double> AlphaProperty =
|
||||
AvaloniaProperty.Register<ImagesSwipeControl, double>(nameof(Alpha), 0.5);
|
||||
|
||||
public double Alpha
|
||||
{
|
||||
get => GetValue(AlphaProperty);
|
||||
set => SetValue(AlphaProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> OldImageProperty =
|
||||
AvaloniaProperty.Register<ImagesSwipeControl, Bitmap>(nameof(OldImage), null);
|
||||
|
||||
public Bitmap OldImage
|
||||
{
|
||||
get => GetValue(OldImageProperty);
|
||||
set => SetValue(OldImageProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> NewImageProperty =
|
||||
AvaloniaProperty.Register<ImagesSwipeControl, Bitmap>(nameof(NewImage), null);
|
||||
|
||||
public Bitmap NewImage
|
||||
{
|
||||
get => GetValue(NewImageProperty);
|
||||
set => SetValue(NewImageProperty, value);
|
||||
}
|
||||
|
||||
static ImagesSwipeControl()
|
||||
{
|
||||
AffectsMeasure<ImagesSwipeControl>(OldImageProperty, NewImageProperty);
|
||||
AffectsRender<ImagesSwipeControl>(AlphaProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
var alpha = Alpha;
|
||||
var w = Bounds.Width;
|
||||
var h = Bounds.Height;
|
||||
var x = w * alpha;
|
||||
var left = OldImage;
|
||||
if (left != null && alpha > 0)
|
||||
{
|
||||
var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height);
|
||||
var dst = new Rect(0, 0, x, h);
|
||||
context.DrawImage(left, src, dst);
|
||||
}
|
||||
|
||||
var right = NewImage;
|
||||
if (right != null && alpha < 1)
|
||||
{
|
||||
var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height);
|
||||
var dst = new Rect(x, 0, w - x, h);
|
||||
context.DrawImage(right, src, dst);
|
||||
}
|
||||
|
||||
context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height));
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
|
||||
var p = e.GetPosition(this);
|
||||
var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height);
|
||||
var pointer = e.GetCurrentPoint(this);
|
||||
if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p))
|
||||
{
|
||||
_pressedOnSlider = true;
|
||||
Cursor = new Cursor(StandardCursorType.SizeWestEast);
|
||||
e.Pointer.Capture(this);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
_pressedOnSlider = false;
|
||||
}
|
||||
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
var w = Bounds.Width;
|
||||
var p = e.GetPosition(this);
|
||||
|
||||
if (_pressedOnSlider)
|
||||
{
|
||||
SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w);
|
||||
}
|
||||
else
|
||||
{
|
||||
var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height);
|
||||
if (hitbox.Contains(p))
|
||||
{
|
||||
if (!_lastInSlider)
|
||||
{
|
||||
_lastInSlider = true;
|
||||
Cursor = new Cursor(StandardCursorType.SizeWestEast);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_lastInSlider)
|
||||
{
|
||||
_lastInSlider = false;
|
||||
Cursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var left = OldImage;
|
||||
var right = NewImage;
|
||||
|
||||
if (left == null)
|
||||
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
|
||||
|
||||
if (right == null)
|
||||
return GetDesiredSize(left.Size, availableSize);
|
||||
|
||||
var ls = GetDesiredSize(left.Size, availableSize);
|
||||
var rs = GetDesiredSize(right.Size, availableSize);
|
||||
return ls.Width > rs.Width ? ls : rs;
|
||||
}
|
||||
|
||||
private Size GetDesiredSize(Size img, Size available)
|
||||
{
|
||||
var w = available.Width;
|
||||
var h = available.Height;
|
||||
|
||||
var sw = available.Width / img.Width;
|
||||
var sh = available.Height / img.Height;
|
||||
var scale = Math.Min(sw, sh);
|
||||
|
||||
return new Size(scale * img.Width, scale * img.Height);
|
||||
}
|
||||
|
||||
private bool _pressedOnSlider = false;
|
||||
private bool _lastInSlider = false;
|
||||
}
|
||||
|
||||
public class ImageBlendControl : Control
|
||||
{
|
||||
public static readonly StyledProperty<double> AlphaProperty =
|
||||
AvaloniaProperty.Register<ImageBlendControl, double>(nameof(Alpha), 1.0);
|
||||
|
||||
public double Alpha
|
||||
{
|
||||
get => GetValue(AlphaProperty);
|
||||
set => SetValue(AlphaProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> OldImageProperty =
|
||||
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(OldImage), null);
|
||||
|
||||
public Bitmap OldImage
|
||||
{
|
||||
get => GetValue(OldImageProperty);
|
||||
set => SetValue(OldImageProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Bitmap> NewImageProperty =
|
||||
AvaloniaProperty.Register<ImageBlendControl, Bitmap>(nameof(NewImage), null);
|
||||
|
||||
public Bitmap NewImage
|
||||
{
|
||||
get => GetValue(NewImageProperty);
|
||||
set => SetValue(NewImageProperty, value);
|
||||
}
|
||||
|
||||
static ImageBlendControl()
|
||||
{
|
||||
AffectsMeasure<ImageBlendControl>(OldImageProperty, NewImageProperty);
|
||||
AffectsRender<ImageBlendControl>(AlphaProperty);
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
var rect = new Rect(0, 0, Bounds.Width, Bounds.Height);
|
||||
var alpha = Alpha;
|
||||
var left = OldImage;
|
||||
if (left != null && alpha < 1)
|
||||
{
|
||||
var state = context.PushOpacity(1- alpha);
|
||||
context.DrawImage(left, rect);
|
||||
state.Dispose();
|
||||
}
|
||||
|
||||
var right = NewImage;
|
||||
if (right != null && alpha > 0)
|
||||
{
|
||||
var state = context.PushOpacity(alpha);
|
||||
context.DrawImage(right, rect);
|
||||
state.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var left = OldImage;
|
||||
var right = NewImage;
|
||||
|
||||
if (left == null)
|
||||
return right == null ? availableSize : GetDesiredSize(right.Size, availableSize);
|
||||
|
||||
if (right == null)
|
||||
return GetDesiredSize(left.Size, availableSize);
|
||||
|
||||
var ls = GetDesiredSize(left.Size, availableSize);
|
||||
var rs = GetDesiredSize(right.Size, availableSize);
|
||||
return ls.Width > rs.Width ? ls : rs;
|
||||
}
|
||||
|
||||
private Size GetDesiredSize(Size img, Size available)
|
||||
{
|
||||
var w = available.Width;
|
||||
var h = available.Height;
|
||||
|
||||
var sw = available.Width / img.Width;
|
||||
var sh = available.Height / img.Height;
|
||||
var scale = Math.Min(sw, sh);
|
||||
|
||||
return new Size(scale * img.Width, scale * img.Height);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ImageDiffView : UserControl
|
||||
{
|
||||
public ImageDiffView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
<TextBlock x:Name="txtDesc"
|
||||
HorizontalAlignment="Stretch"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Decrease}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
|
||||
FontStyle="Italic"
|
||||
Text="{Binding #me.Description}"/>
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
Classes="italic"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:FontSizeModifyConverters.Decrease}}"
|
||||
FontSize="{Binding Source={x:Static vm:Preference.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Decrease}}"
|
||||
Text="{DynamicResource Text.Welcome.DragDropTip}"
|
||||
Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</Grid>
|
||||
|
|
Loading…
Reference in a new issue