Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Pinta.Core/Classes/DisposableUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Threading;

namespace Pinta.Core;

public static class DisposableUtilities
{
public static IDisposable FromAction (Action action)
{
return new ActionDisposable (action);
}

private sealed class ActionDisposable (Action action) : IDisposable
{
private Action? action = action;
public void Dispose ()
{
Interlocked.Exchange (ref action, null)?.Invoke ();
}
}
}
19 changes: 19 additions & 0 deletions Pinta.Core/Classes/ReactiveUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;

namespace Pinta.Core;

public static class ReactiveUtilities
{
public static void NotifyAll<TMessage> (this LinkedList<IObserver<TMessage>> listeners, TMessage message)
{
foreach (var listener in listeners)
listener.OnNext (message);
}

public static IDisposable Subscribe<TMessage> (this LinkedList<IObserver<TMessage>> listeners, IObserver<TMessage> @new)
{
var newNode = listeners.AddLast (@new);
return DisposableUtilities.FromAction (() => listeners.Remove (newNode));
}
}
8 changes: 8 additions & 0 deletions Pinta.Core/Enumerations/MetricType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Pinta.Core;

public enum MetricType
{
Pixels,
Inches,
Centimeters,
}
17 changes: 17 additions & 0 deletions Pinta.Core/Models/RulerModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Pinta.Core;

public sealed class RulerModel (Gtk.Orientation orientation)
{
/// <summary>The position of the mark along the ruler.</summary>
public double Position { get; set; } = 0;

/// <summary>Metric type used for the ruler.</summary>
public MetricType Metric { get; set; } = MetricType.Pixels;

public NumberRange<double>? SelectionBounds { get; set; } = null;

public NumberRange<double> RulerRange { get; set; } = default;

/// <summary>Whether the ruler is horizontal or vertical.</summary>
public Gtk.Orientation Orientation { get; } = orientation;
}
22 changes: 15 additions & 7 deletions Pinta.Gui.Widgets/Widgets/Canvas/CanvasWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public sealed class CanvasWindow : Gtk.Grid
private readonly ChromeManager chrome;
private readonly ToolManager tools;

private readonly Ruler horizontal_ruler;
private readonly Ruler vertical_ruler;
private readonly RulerViewModel horizontal_ruler;
private readonly RulerViewModel vertical_ruler;
private readonly Gtk.ScrolledWindow scrolled_window;
private readonly Gtk.EventControllerMotion motion_controller;
private readonly Gtk.GestureDrag drag_controller;
Expand Down Expand Up @@ -97,13 +97,21 @@ public CanvasWindow (
Child = viewPort,
};

Ruler horizontalRuler = new (Gtk.Orientation.Horizontal) {
RulerModel horizontalRulerModel = new (Gtk.Orientation.Horizontal) {
Metric = MetricType.Pixels,
};
RulerModel verticalRulerModel = new (Gtk.Orientation.Vertical) {
Metric = MetricType.Pixels,
};

RulerViewModel horizontalRulerViewModel = new (horizontalRulerModel);
RulerViewModel verticalRulerViewModel = new (verticalRulerModel);

RulerView horizontalRuler = new (horizontalRulerViewModel) {
Visible = false,
};

Ruler verticalRuler = new (Gtk.Orientation.Vertical) {
Metric = MetricType.Pixels,
RulerView verticalRuler = new (verticalRulerViewModel) {
Visible = false,
};

Expand Down Expand Up @@ -139,8 +147,8 @@ public CanvasWindow (

scrolled_window = scrolledWindow;
gesture_zoom = gestureZoom;
horizontal_ruler = horizontalRuler;
vertical_ruler = verticalRuler;
horizontal_ruler = horizontalRulerViewModel;
vertical_ruler = verticalRulerViewModel;
motion_controller = motionController;
drag_controller = dragController;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,110 +33,38 @@

namespace Pinta.Gui.Widgets;

public enum MetricType
{
Pixels,
Inches,
Centimeters,
}

/// <summary>
/// Replacement for Gtk.Ruler, which was removed in GTK3.
/// Based on the original GTK2 widget and Inkscape's ruler widget.
/// </summary>
public sealed class Ruler : Gtk.DrawingArea
public sealed class RulerView : Gtk.DrawingArea,
IObserver<RulerChanged>,
IObserver<RulerVisibilityChanged>
{
private double position = 0;
private MetricType metric = MetricType.Pixels;

private Surface? cached_surface = null;
private Size? last_known_size = null;

private NumberRange<double>? selection_bounds = null;
public NumberRange<double>? SelectionBounds {
get => selection_bounds;
set {
if (selection_bounds == value) return;
selection_bounds = value;
QueueDraw ();
}
}

/// <summary>
/// Whether the ruler is horizontal or vertical.
/// </summary>
public Gtk.Orientation Orientation { get; }

/// <summary>
/// Metric type used for the ruler.
/// </summary>
public MetricType Metric {
get => metric;
set {
if (metric == value) return;
metric = value;
QueueFullRedraw ();
}
}

/// <summary>
/// The position of the mark along the ruler.
/// </summary>
public double Position {
get => position;
set {
if (position == value) return;
position = value;
QueueFullRedraw ();
}
}
private readonly RulerViewModel view_model;

private NumberRange<double> ruler_range;
public NumberRange<double> RulerRange {
get => ruler_range;
set {
if (ruler_range == value) return;
ruler_range = value;
QueueFullRedraw ();
}
}

public Ruler (Gtk.Orientation orientation)
public RulerView (RulerViewModel viewModel)
{
Orientation = orientation;
view_model = viewModel;

SetDrawFunc ((area, context, width, height) => Draw (context, new Size (width, height)));

// Determine the size request, based on the font size.
int font_size = GetFontSize (GetPangoContext ().GetFontDescription ()!, ScaleFactor);
int size = 2 + font_size * 2;

int width = 0;
int height = 0;
switch (Orientation) {
case Gtk.Orientation.Horizontal:
height = size;
break;
case Gtk.Orientation.Vertical:
width = size;
break;
}

WidthRequest = width;
HeightRequest = height;
}

// Invalidates cache _and_ queues redraw. Like a full refresh
private void QueueFullRedraw ()
{
InvalidateCache ();
QueueDraw ();
}
int measure = 2 + font_size * 2;
Size size = viewModel.Orientation switch {
Gtk.Orientation.Horizontal => new Size (Width: 0, Height: measure),
Gtk.Orientation.Vertical => new Size (Width: measure, Height: 0),
_ => throw new UnreachableException (),
};
WidthRequest = size.Width;
HeightRequest = size.Height;

private void InvalidateCache ()
{
cached_surface?.Dispose ();
cached_surface = null;
viewModel.Subscribe ((IObserver<RulerChanged>) this);
viewModel.Subscribe ((IObserver<RulerVisibilityChanged>) this);
}

private static readonly ImmutableArray<double> pixels_ruler_scale = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000];
Expand Down Expand Up @@ -168,7 +96,7 @@ private RulerDrawSettings CreateSettings (Size preliminarySize)
{
GetStyleContext ().GetColor (out Gdk.RGBA color);

RectangleD rulerOuterLine = Orientation switch {
RectangleD rulerOuterLine = view_model.Orientation switch {

Gtk.Orientation.Vertical => new (
X: preliminarySize.Width - 1,
Expand All @@ -185,27 +113,27 @@ private RulerDrawSettings CreateSettings (Size preliminarySize)
_ => throw new UnreachableException (),
};

Size effectiveSize = Orientation switch {
Size effectiveSize = view_model.Orientation switch {
Gtk.Orientation.Vertical => new (preliminarySize.Height, preliminarySize.Width),// Swap so that width is the longer dimension (horizontal).
Gtk.Orientation.Horizontal => preliminarySize,
_ => throw new UnreachableException (),
};

ImmutableArray<double> rulerScale = Metric switch {
ImmutableArray<double> rulerScale = view_model.Metric switch {
MetricType.Pixels => pixels_ruler_scale,
MetricType.Inches => inches_ruler_scale,
MetricType.Centimeters => centimeters_ruler_scale,
_ => throw new UnreachableException (),
};

ImmutableArray<int> subdivide = Metric switch {
ImmutableArray<int> subdivide = view_model.Metric switch {
MetricType.Pixels => pixels_subdivide,
MetricType.Inches => inches_subdivide,
MetricType.Centimeters => centimeters_subdivide,
_ => throw new UnreachableException (),
};

double pixels_per_unit = Metric switch {
double pixels_per_unit = view_model.Metric switch {
MetricType.Pixels => 1.0,
MetricType.Inches => 72,
MetricType.Centimeters => 28.35,
Expand All @@ -215,8 +143,8 @@ private RulerDrawSettings CreateSettings (Size preliminarySize)
// Find our scaled range.

NumberRange<double> scaledRange = new (
lower: RulerRange.Lower / pixels_per_unit,
upper: RulerRange.Upper / pixels_per_unit);
lower: view_model.RulerRange.Lower / pixels_per_unit,
upper: view_model.RulerRange.Upper / pixels_per_unit);

double maxSize = scaledRange.Upper - scaledRange.Lower;

Expand Down Expand Up @@ -257,11 +185,11 @@ private RulerDrawSettings CreateSettings (Size preliminarySize)
Ticks: new (
lower: (int) Math.Floor (scaledRange.Lower * ticksPerUnit),
upper: (int) Math.Ceiling (scaledRange.Upper * ticksPerUnit)),
MarkerPosition: GetPositionOnRuler (Position, effectiveSize.Width),
MarkerPosition: GetPositionOnRuler (view_model.Position, effectiveSize.Width),
RulerOuterLine: rulerOuterLine,
EffectiveSize: effectiveSize,
Color: color.ToCairoColor (),
Orientation: Orientation);
Orientation: view_model.Orientation);
}

private void Draw (Context cr, Size preliminarySize)
Expand All @@ -276,19 +204,19 @@ private void Draw (Context cr, Size preliminarySize)
cached_surface ??= CreateBaseRuler (settings, preliminarySize);

// Draw the selection projection if a selection exists
if (selection_bounds.HasValue) {
if (view_model.SelectionBounds.HasValue) {

// Convert selection coordinates to ruler widget coordinates
double p1 = GetPositionOnRuler (selection_bounds.Value.Lower, settings.EffectiveSize.Width);
double p2 = GetPositionOnRuler (selection_bounds.Value.Upper, settings.EffectiveSize.Width);
double p1 = GetPositionOnRuler (view_model.SelectionBounds.Value.Lower, settings.EffectiveSize.Width);
double p2 = GetPositionOnRuler (view_model.SelectionBounds.Value.Upper, settings.EffectiveSize.Width);

cr.SetSourceRgba ( // Semi-transparent blue
red: 0.21,
green: 0.52,
blue: 0.89,
alpha: 0.25);

switch (Orientation) {
switch (view_model.Orientation) {
case Gtk.Orientation.Horizontal:
cr.Rectangle (p1, 0, p2 - p1, settings.EffectiveSize.Height);
break;
Expand Down Expand Up @@ -384,9 +312,9 @@ private ImageSurface CreateBaseRuler (in RulerDrawSettings settings, Size prelim
[MethodImpl (MethodImplOptions.AggressiveInlining)]
private double GetPositionOnRuler (double position, double width)
{
double range = RulerRange.Upper - RulerRange.Lower;
double range = view_model.RulerRange.Upper - view_model.RulerRange.Lower;
double scaledWidth = width / range;
double positionFromLower = position - RulerRange.Lower;
double positionFromLower = position - view_model.RulerRange.Lower;
return positionFromLower * scaledWidth;
}

Expand All @@ -398,4 +326,35 @@ private static int GetFontSize (Pango.FontDescription font, int scaleFactor)
else
return (int) (scaleFactor * fontSize / 72.0);
}

private void InvalidateCache ()
{
cached_surface?.Dispose ();
cached_surface = null;
}

private void QueueFullRedraw ()
{
InvalidateCache ();
QueueDraw ();
}

public void OnNext (RulerChanged value)
{
if (value.Full)
QueueFullRedraw ();
else
QueueDraw ();
}

public void OnNext (RulerVisibilityChanged value)
{
Visible = value.NewVisibility;
}

void IObserver<RulerChanged>.OnCompleted () { }
void IObserver<RulerChanged>.OnError (Exception error) => throw new NotImplementedException ();

void IObserver<RulerVisibilityChanged>.OnCompleted () { }
void IObserver<RulerVisibilityChanged>.OnError (Exception error) => throw new NotImplementedException ();
}
Loading
Loading