diff --git a/demo/Ursa.Demo/Assets/WORLD.png b/demo/Ursa.Demo/Assets/WORLD.png new file mode 100644 index 0000000..7c5b71f Binary files /dev/null and b/demo/Ursa.Demo/Assets/WORLD.png differ diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 1d8954e..9db0558 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -7,6 +7,7 @@ public static class MenuKeys public const string MenuKeyButtonGroup = "ButtonGroup"; public const string MenuKeyDivider = "Divider"; public const string MenuKeyDualBadge = "DualBadge"; + public const string MenuKeyImageViewer = "ImageViewer"; public const string MenuKeyIpBox = "IPv4Box"; public const string MenuKeyLoading = "Loading"; public const string MenuKeyNavigation = "Navigation"; diff --git a/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml b/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml new file mode 100644 index 0000000..67d03a8 --- /dev/null +++ b/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml.cs b/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml.cs new file mode 100644 index 0000000..7aa908c --- /dev/null +++ b/demo/Ursa.Demo/Pages/ImageViewerDemo.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class ImageViewerDemo : UserControl +{ + public ImageViewerDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Ursa.Demo.csproj b/demo/Ursa.Demo/Ursa.Demo.csproj index 4f0188d..d11b6e7 100644 --- a/demo/Ursa.Demo/Ursa.Demo.csproj +++ b/demo/Ursa.Demo/Ursa.Demo.csproj @@ -7,23 +7,23 @@ - + - + - + - - - + + + - - + + diff --git a/demo/Ursa.Demo/ViewModels/ImageViewerDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ImageViewerDemoViewModel.cs new file mode 100644 index 0000000..9faefee --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/ImageViewerDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class ImageViewerDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 54109ae..56b418f 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -29,6 +29,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(), + MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(), MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(), MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(), MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 011375b..de33560 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -16,6 +16,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup }, new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider }, new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge }, + new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer }, new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation }, diff --git a/src/Ursa.Themes.Semi/Controls/ImageViewer.axaml b/src/Ursa.Themes.Semi/Controls/ImageViewer.axaml new file mode 100644 index 0000000..3a8b234 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/ImageViewer.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index dee9b3c..7103216 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -6,6 +6,7 @@ + diff --git a/src/Ursa/Controls/ImageViewer/ImageViewer.cs b/src/Ursa/Controls/ImageViewer/ImageViewer.cs new file mode 100644 index 0000000..ca77270 --- /dev/null +++ b/src/Ursa/Controls/ImageViewer/ImageViewer.cs @@ -0,0 +1,153 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; + +namespace Ursa.Controls; + +[TemplatePart(PART_Image, typeof(Image))] +[TemplatePart(PART_Layer, typeof(VisualLayerManager))] +public class ImageViewer: TemplatedControl +{ + public const string PART_Image = "PART_Image"; + public const string PART_Layer = "PART_Layer"; + + private Image _image = null!; + private VisualLayerManager? _layer; + private Point? _lastClickPoint; + private Point? _lastReleasePoint; + + public static readonly StyledProperty OverlayerProperty = AvaloniaProperty.Register( + nameof(Overlayer)); + + public object? Overlayer + { + get => GetValue(OverlayerProperty); + set => SetValue(OverlayerProperty, value); + } + + public static readonly StyledProperty SourceProperty = Image.SourceProperty.AddOwner(); + public IImage? Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + private double _scale = 1; + + public static readonly DirectProperty ScaleProperty = AvaloniaProperty.RegisterDirect( + nameof(Scale), o => o.Scale, unsetValue: 1); + + public double Scale + { + get => _scale; + private set => SetAndRaise(ScaleProperty, ref _scale, value); + } + + private double _translateX; + + public static readonly DirectProperty TranslateXProperty = AvaloniaProperty.RegisterDirect( + nameof(TranslateX), o => o.TranslateX, unsetValue: 0); + + public double TranslateX + { + get => _translateX; + private set => SetAndRaise(TranslateXProperty, ref _translateX, value); + } + + private double _translateY; + + public static readonly DirectProperty TranslateYProperty = AvaloniaProperty.RegisterDirect( + nameof(TranslateY), o => o.TranslateY); + + public double TranslateY + { + get => _translateY; + private set => SetAndRaise(TranslateYProperty, ref _translateY, value); + } + + + static ImageViewer() + { + OverlayerProperty.Changed.AddClassHandler((o, e) => o.OnOverlayerChanged(e)); + SourceProperty.Changed.AddClassHandler((o, e) => o.OnSourceChanged(e)); + } + + private void OnOverlayerChanged(AvaloniaPropertyChangedEventArgs args) + { + var control = args.GetNewValue(); + if (control is Control c) + { + AdornerLayer.SetAdorner(this, c); + } + } + + private void OnSourceChanged(AvaloniaPropertyChangedEventArgs args) + { + IImage image = args.GetNewValue(); + Size size = image.Size; + double width = this.Width; + double height = this.Height; + Scale = Math.Min(width/size.Width, height/size.Height); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _image = e.NameScope.Get(PART_Image); + _layer = e.NameScope.Get(PART_Layer); + Scale = 1; + if (Overlayer is Control c) + { + AdornerLayer.SetAdorner(this, c); + } + } + + + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + if(e.Delta.Y > 0) + { + Scale *= 1.1; + } + else + { + var scale = Scale; + scale /= 1.1; + if (scale < 0.1) scale = 0.1; + Scale = scale; + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + if (e.Pointer.Captured == this && _lastClickPoint != null) + { + Point p = e.GetPosition(this); + double deltaX = p.X - _lastClickPoint.Value.X; + double deltaY = p.Y - _lastClickPoint.Value.Y; + TranslateX = deltaX + (_lastReleasePoint?.X ?? 0); + TranslateY = deltaY + (_lastReleasePoint?.Y ?? 0); + } + + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + e.Pointer.Capture(this); + _lastClickPoint = e.GetPosition(this); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + e.Pointer.Capture(null); + _lastReleasePoint = new Point(TranslateX, TranslateY); + } +} \ No newline at end of file