mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-11 15:26:55 +03:00
227 lines
8.5 KiB
C#
227 lines
8.5 KiB
C#
using Eto.Drawing;
|
|
using Eto.Forms;
|
|
using NAPS2.EtoForms.Layout;
|
|
|
|
namespace NAPS2.EtoForms.Widgets;
|
|
|
|
public class ScrollZoomImageViewer
|
|
{
|
|
private readonly Scrollable _scrollable = new() { Border = BorderType.None };
|
|
private readonly Drawable _imageView = new() { BackgroundColor = Colors.White };
|
|
private PointF _scrollableLocation;
|
|
private Size _renderSize;
|
|
private float _renderFactor;
|
|
private PointF? _mousePos;
|
|
private PointF? _initialMousePos;
|
|
private Point? _initialScrollPos;
|
|
private bool _mouseIsDown;
|
|
|
|
public ScrollZoomImageViewer()
|
|
{
|
|
_scrollable.Content = _imageView;
|
|
_scrollable.BackgroundColor = Colors.White;
|
|
_imageView.Paint += ImagePaint;
|
|
_scrollable.Shown += OnShown;
|
|
_scrollable.MouseEnter += OnMouseEnter;
|
|
_scrollable.MouseLeave += OnMouseLeave;
|
|
_scrollable.MouseMove += OnMouseMove;
|
|
_scrollable.MouseDown += OnMouseDown;
|
|
_scrollable.MouseUp += OnMouseUp;
|
|
EtoPlatform.Current.AttachMouseWheelEvent(_scrollable, OnMouseWheel);
|
|
_scrollable.Cursor = Cursors.Pointer;
|
|
}
|
|
|
|
// For some reason Gtk can't render a few pixels width around the edge of the image, so we need to correct for that.
|
|
private int BorderOffset { get; } = EtoPlatform.Current.IsGtk ? 6 : 0;
|
|
|
|
public Bitmap? Image { get; set; }
|
|
|
|
public ColorScheme? ColorScheme { get; set; }
|
|
|
|
public event EventHandler<ZoomChangedEventArgs>? ZoomChanged;
|
|
|
|
private Size RenderSize
|
|
{
|
|
get => _renderSize;
|
|
set
|
|
{
|
|
_renderSize = value;
|
|
_imageView.Size = Size.Round(value) + new Size(2, 2);
|
|
_imageView.Invalidate();
|
|
}
|
|
}
|
|
|
|
private int AvailableWidth => _scrollable.Width - 2 - BorderOffset;
|
|
private int AvailableHeight => _scrollable.Height - 2 - BorderOffset;
|
|
|
|
private bool IsWidthBound =>
|
|
Image!.Width / (float) Image.Height > AvailableWidth / (float) AvailableHeight;
|
|
|
|
private bool FitsInBounds => RenderSize.Width <= AvailableWidth && RenderSize.Height <= AvailableHeight;
|
|
|
|
private float XOffset => Math.Max((_imageView.Width - RenderSize.Width) / 2, 0);
|
|
private float YOffset => Math.Max((_imageView.Height - RenderSize.Height) / 2, 0);
|
|
|
|
private void OnShown(object? sender, EventArgs e)
|
|
{
|
|
_scrollableLocation = _scrollable.Location;
|
|
}
|
|
|
|
private void OnMouseEnter(object? sender, MouseEventArgs e)
|
|
{
|
|
// TODO: Mouse enter/leave events aren't firing on WinForms, why?
|
|
_mousePos = e.Location;
|
|
}
|
|
|
|
private void OnMouseUp(object? sender, MouseEventArgs e)
|
|
{
|
|
_mouseIsDown = false;
|
|
}
|
|
|
|
private void OnMouseDown(object? sender, MouseEventArgs e)
|
|
{
|
|
_mouseIsDown = true;
|
|
_initialMousePos = e.Location;
|
|
_initialScrollPos = _scrollable.ScrollPosition;
|
|
}
|
|
|
|
private void OnMouseMove(object? sender, MouseEventArgs e)
|
|
{
|
|
if (_mouseIsDown && _initialMousePos.HasValue && _initialScrollPos.HasValue)
|
|
{
|
|
_scrollable.ScrollPosition = _initialScrollPos.Value + Point.Round(_initialMousePos.Value - e.Location);
|
|
}
|
|
_mousePos = e.Location;
|
|
}
|
|
|
|
private void OnMouseLeave(object? sender, MouseEventArgs e)
|
|
{
|
|
_mousePos = null;
|
|
}
|
|
|
|
private void OnMouseWheel(object? sender, MouseEventArgs e)
|
|
{
|
|
if (e.Modifiers == Keys.Control)
|
|
{
|
|
ChangeZoom(e.Delta.Height, true);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void ImagePaint(object? sender, PaintEventArgs e)
|
|
{
|
|
var bgColor = ColorScheme?.BackgroundColor ?? Colors.White;
|
|
e.Graphics.FillRectangle(bgColor, 0, 0, _imageView.Width, _imageView.Height);
|
|
if (Image != null)
|
|
{
|
|
e.Graphics.DrawRectangle(
|
|
ColorScheme?.BorderColor ?? Colors.Black,
|
|
XOffset - 1, YOffset - 1, RenderSize.Width + 1, RenderSize.Height + 1);
|
|
e.Graphics.DrawImage(Image, XOffset, YOffset, RenderSize.Width, RenderSize.Height);
|
|
}
|
|
}
|
|
|
|
public float ZoomFactor => _renderFactor;
|
|
|
|
// GDI doesn't like when the render size (4 bytes per pixel) exceeds int.MaxValue
|
|
public float MaxZoom => (float) Math.Sqrt(int.MaxValue * 0.99 / (Image!.Width * Image.Height * 4));
|
|
|
|
public void ChangeZoom(float step, bool anchorToMouse = false)
|
|
{
|
|
SetZoom(_renderFactor * (float) Math.Pow(1.2, step), anchorToMouse);
|
|
}
|
|
|
|
public void SetZoom(float value, bool anchorToMouse = false)
|
|
{
|
|
_renderFactor = value.Clamp(0.01f, MaxZoom);
|
|
_scrollable.SuspendLayout();
|
|
var anchor = GetMouseAnchor(anchorToMouse);
|
|
RenderSize =
|
|
Size.Round(new SizeF(Image!.Width * _renderFactor, Image.Height * _renderFactor));
|
|
SetMouseAnchor(anchor);
|
|
_scrollable.ResumeLayout();
|
|
ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor));
|
|
}
|
|
|
|
public void ZoomToActual()
|
|
{
|
|
RenderSize = new Size(Image!.Width, Image.Height);
|
|
_renderFactor = 1f;
|
|
ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor));
|
|
}
|
|
|
|
public void ZoomToContainer()
|
|
{
|
|
if (!_scrollable.Loaded || Image == null || _scrollable.Width <= 0 || _scrollable.Height <= 0)
|
|
{
|
|
return;
|
|
}
|
|
if (IsWidthBound)
|
|
{
|
|
RenderSize = new Size(AvailableWidth,
|
|
(int) Math.Round(Image!.Height * AvailableWidth / (float) Image.Width));
|
|
_renderFactor = AvailableWidth / (float) Image.Width;
|
|
}
|
|
else
|
|
{
|
|
RenderSize =
|
|
new Size((int) Math.Round(Image!.Width * AvailableHeight / (float) Image.Height),
|
|
AvailableHeight);
|
|
_renderFactor = AvailableHeight / (float) Image.Height;
|
|
}
|
|
ZoomChanged?.Invoke(this, new ZoomChangedEventArgs(_renderFactor));
|
|
}
|
|
|
|
// When we zoom in/out (e.g. with Ctrl+mousewheel), we want the point in the image underneath the mouse cursor to
|
|
// stay stationary relative to the mouse (as much as permitted by the available scroll region). If the mouse is not
|
|
// overtop the image, the middle of the image (as currently visible) should stay stationary.
|
|
//
|
|
// This function calculates the image anchor as a fraction (i.e. a point in the range [<0,0>, <1,1>]). It also
|
|
// returns the mouse position relative to the top left of the Scrollable (or the middle of the scrollable if the
|
|
// mouse is not overtop the image).
|
|
private (PointF imageAnchor, PointF mouseRelativePos) GetMouseAnchor(bool anchorToMouse)
|
|
{
|
|
var anchorMiddle = new PointF(0.5f, 0.5f);
|
|
var scrollableMiddle = new PointF(
|
|
_scrollableLocation.X + _scrollable.Width / 2f,
|
|
_scrollableLocation.Y + _scrollable.Height / 2f);
|
|
if (!anchorToMouse ||
|
|
_mousePos is not { } mousePos ||
|
|
mousePos.X < _scrollableLocation.X ||
|
|
mousePos.Y < _scrollableLocation.Y ||
|
|
mousePos.X > _scrollableLocation.X + _scrollable.Width ||
|
|
mousePos.Y > _scrollableLocation.Y + _scrollable.Height)
|
|
{
|
|
// Mouse is outside the scrollable
|
|
return (anchorMiddle, scrollableMiddle);
|
|
}
|
|
var mouseRelativePos = mousePos - _scrollableLocation - new Point(1, 1);
|
|
var x = (mouseRelativePos.X + _scrollable.ScrollPosition.X - XOffset) / RenderSize.Width;
|
|
var y = (mouseRelativePos.Y + _scrollable.ScrollPosition.Y - YOffset) / RenderSize.Height;
|
|
if (x < 0 || y < 0 || x > 1 || y > 1)
|
|
{
|
|
// Mouse is inside the scrollable but outside the area covered by the image
|
|
return (anchorMiddle, scrollableMiddle);
|
|
}
|
|
return (new PointF(x, y), mouseRelativePos);
|
|
}
|
|
|
|
// This function inverts the calculation done in GetMouseAnchor to get the correct scroll position to move the point
|
|
// in the image that was underneath the mouse back there (after the image size has been changed).
|
|
private void SetMouseAnchor((PointF imageAnchor, PointF mouseRelativePos) anchor)
|
|
{
|
|
if (FitsInBounds)
|
|
{
|
|
return;
|
|
}
|
|
// TODO: This is off a bit for the "middle" anchor, probably because the scrollbars themselves appear
|
|
var xScroll = anchor.imageAnchor.X * RenderSize.Width + XOffset - anchor.mouseRelativePos.X;
|
|
var yScroll = anchor.imageAnchor.Y * RenderSize.Height + YOffset - anchor.mouseRelativePos.Y;
|
|
_scrollable.ScrollPosition = Point.Round(new PointF(xScroll, yScroll));
|
|
}
|
|
|
|
public static implicit operator LayoutElement(ScrollZoomImageViewer control)
|
|
{
|
|
return control._scrollable;
|
|
}
|
|
} |