diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 08a9aa3ceb..4f69f39e02 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index b10db08adc..38d99db5c9 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -135,6 +135,7 @@ enum AvnWindowState
Normal,
Minimized,
Maximized,
+ FullScreen,
};
enum AvnStandardCursorType
@@ -246,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0;
- virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
+ virtual HRESULT SetDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0;
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index c30c2c3d18..1e74a70e66 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -15,6 +15,10 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
}
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+
+ [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
+
+ [[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]];
}
}
diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h
index 505900d584..ec8fe9e6ee 100644
--- a/native/Avalonia.Native/src/OSX/window.h
+++ b/native/Avalonia.Native/src/OSX/window.h
@@ -19,6 +19,8 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
+-(bool) isModal;
+-(void) setModal: (bool) isModal;
-(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;
@@ -33,6 +35,10 @@ struct INSWindowHolder
struct IWindowStateChanged
{
virtual void WindowStateChanged () = 0;
+ virtual void StartStateTransition () = 0;
+ virtual void EndStateTransition () = 0;
+ virtual SystemDecorations Decorations () = 0;
+ virtual AvnWindowState WindowState () = 0;
};
#endif /* window_h */
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 109ed63e6f..091219fc72 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -391,7 +391,7 @@ protected:
void UpdateStyle()
{
- [Window setStyleMask:GetStyle()];
+ [Window setStyleMask: GetStyle()];
}
public:
@@ -404,10 +404,13 @@ public:
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{
private:
- bool _canResize = true;
- SystemDecorations _hasDecorations = SystemDecorationsFull;
- CGRect _lastUndecoratedFrame;
+ bool _canResize;
+ bool _fullScreenActive;
+ SystemDecorations _decorations;
AvnWindowState _lastWindowState;
+ bool _inSetWindowState;
+ NSRect _preZoomSize;
+ bool _transitioningWindowState;
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
@@ -421,10 +424,30 @@ private:
ComPtr WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{
+ _fullScreenActive = false;
+ _canResize = true;
+ _decorations = SystemDecorationsFull;
+ _transitioningWindowState = false;
+ _inSetWindowState = false;
_lastWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
+ [Window setTabbingMode:NSWindowTabbingModeDisallowed];
+ }
+
+ void HideOrShowTrafficLights ()
+ {
+ for (id subview in Window.contentView.superview.subviews) {
+ if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
+ NSView *titlebarView = [subview subviews][0];
+ for (id button in titlebarView.subviews) {
+ if ([button isKindOfClass:[NSButton class]]) {
+ [button setHidden: (_decorations != SystemDecorationsFull)];
+ }
+ }
+ }
+ }
}
virtual HRESULT Show () override
@@ -433,8 +456,13 @@ private:
{
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
+
+ [Window setModal:FALSE];
+
WindowBaseImpl::Show();
+ HideOrShowTrafficLights();
+
return SetWindowState(_lastWindowState);
}
}
@@ -450,44 +478,74 @@ private:
if(cparent == nullptr)
return E_INVALIDARG;
+ [Window setModal:TRUE];
+
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
+ HideOrShowTrafficLights();
+
return S_OK;
}
}
+ void StartStateTransition () override
+ {
+ _transitioningWindowState = true;
+ }
+
+ void EndStateTransition () override
+ {
+ _transitioningWindowState = false;
+ }
+
+ SystemDecorations Decorations () override
+ {
+ return _decorations;
+ }
+
+ AvnWindowState WindowState () override
+ {
+ return _lastWindowState;
+ }
+
void WindowStateChanged () override
{
- AvnWindowState state;
- GetWindowState(&state);
- WindowEvents->WindowStateChanged(state);
+ if(!_inSetWindowState && !_transitioningWindowState)
+ {
+ AvnWindowState state;
+ GetWindowState(&state);
+
+ if(_lastWindowState != state)
+ {
+ _lastWindowState = state;
+ WindowEvents->WindowStateChanged(state);
+ }
+ }
}
bool UndecoratedIsMaximized ()
{
- return CGRectEqualToRect([Window frame], [Window screen].visibleFrame);
+ auto windowSize = [Window frame];
+ auto available = [Window screen].visibleFrame;
+ return CGRectEqualToRect(windowSize, available);
}
bool IsZoomed ()
{
- return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
+ return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized();
}
void DoZoom()
{
- switch (_hasDecorations)
+ switch (_decorations)
{
case SystemDecorationsNone:
- if (!UndecoratedIsMaximized())
- {
- _lastUndecoratedFrame = [Window frame];
- }
-
- [Window zoom:Window];
+ case SystemDecorationsBorderOnly:
+ [Window setFrame:[Window screen].visibleFrame display:true];
break;
- case SystemDecorationsBorderOnly:
+
case SystemDecorationsFull:
[Window performZoom:Window];
break;
@@ -504,25 +562,52 @@ private:
}
}
- virtual HRESULT SetHasDecorations(SystemDecorations value) override
+ virtual HRESULT SetDecorations(SystemDecorations value) override
{
@autoreleasepool
{
- _hasDecorations = value;
+ auto currentWindowState = _lastWindowState;
+ _decorations = value;
+
+ if(_fullScreenActive)
+ {
+ return S_OK;
+ }
+
+ auto currentFrame = [Window frame];
+
UpdateStyle();
+
+ HideOrShowTrafficLights();
- switch (_hasDecorations)
+ switch (_decorations)
{
case SystemDecorationsNone:
[Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
+
+ if(currentWindowState == Maximized)
+ {
+ if(!UndecoratedIsMaximized())
+ {
+ DoZoom();
+ }
+ }
break;
case SystemDecorationsBorderOnly:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
+
+ if(currentWindowState == Maximized)
+ {
+ if(!UndecoratedIsMaximized())
+ {
+ DoZoom();
+ }
+ }
break;
case SystemDecorationsFull:
@@ -530,6 +615,13 @@ private:
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
+
+ if(currentWindowState == Maximized)
+ {
+ auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
+
+ [View setFrameSize:newFrame];
+ }
break;
}
@@ -586,13 +678,19 @@ private:
return E_POINTER;
}
+ if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen)
+ {
+ *ret = FullScreen;
+ return S_OK;
+ }
+
if([Window isMiniaturized])
{
*ret = Minimized;
return S_OK;
}
- if([Window isZoomed])
+ if(IsZoomed())
{
*ret = Maximized;
return S_OK;
@@ -604,16 +702,57 @@ private:
}
}
+ void EnterFullScreenMode ()
+ {
+ _fullScreenActive = true;
+
+ [Window setHasShadow:YES];
+ [Window setTitleVisibility:NSWindowTitleVisible];
+ [Window setTitlebarAppearsTransparent:NO];
+ [Window setTitle:_lastTitle];
+
+ [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable];
+
+ [Window toggleFullScreen:nullptr];
+ }
+
+ void ExitFullScreenMode ()
+ {
+ [Window toggleFullScreen:nullptr];
+
+ _fullScreenActive = false;
+
+ SetDecorations(_decorations);
+ }
+
virtual HRESULT SetWindowState (AvnWindowState state) override
{
@autoreleasepool
{
+ if(_lastWindowState == state)
+ {
+ return S_OK;
+ }
+
+ _inSetWindowState = true;
+
+ auto currentState = _lastWindowState;
_lastWindowState = state;
+ if(currentState == Normal)
+ {
+ _preZoomSize = [Window frame];
+ }
+
if(_shown)
{
switch (state) {
case Maximized:
+ if(currentState == FullScreen)
+ {
+ ExitFullScreenMode();
+ }
+
lastPositionSet.X = 0;
lastPositionSet.Y = 0;
@@ -629,40 +768,66 @@ private:
break;
case Minimized:
- [Window miniaturize:Window];
+ if(currentState == FullScreen)
+ {
+ ExitFullScreenMode();
+ }
+ else
+ {
+ [Window miniaturize:Window];
+ }
+ break;
+
+ case FullScreen:
+ if([Window isMiniaturized])
+ {
+ [Window deminiaturize:Window];
+ }
+
+ EnterFullScreenMode();
break;
- default:
+ case Normal:
if([Window isMiniaturized])
{
[Window deminiaturize:Window];
}
+ if(currentState == FullScreen)
+ {
+ ExitFullScreenMode();
+ }
+
if(IsZoomed())
{
- DoZoom();
+ if(_decorations == SystemDecorationsFull)
+ {
+ DoZoom();
+ }
+ else
+ {
+ [Window setFrame:_preZoomSize display:true];
+ auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
+
+ [View setFrameSize:newFrame];
+ }
+
}
break;
}
}
+ _inSetWindowState = false;
+
return S_OK;
}
}
virtual void OnResized () override
{
- if(_shown)
+ if(_shown && !_inSetWindowState && !_transitioningWindowState)
{
- auto windowState = [Window isMiniaturized] ? Minimized
- : (IsZoomed() ? Maximized : Normal);
-
- if (windowState != _lastWindowState)
- {
- _lastWindowState = windowState;
-
- WindowEvents->WindowStateChanged(windowState);
- }
+ WindowStateChanged();
}
}
@@ -671,22 +836,23 @@ protected:
{
unsigned long s = NSWindowStyleMaskBorderless;
- switch (_hasDecorations)
+ switch (_decorations)
{
case SystemDecorationsNone:
+ s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break;
case SystemDecorationsBorderOnly:
- s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
+ s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
+
if(_canResize)
{
s = s | NSWindowStyleMaskResizable;
}
-
break;
}
@@ -1144,6 +1310,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr _parent;
bool _canBecomeKeyAndMain;
bool _closed;
+ bool _isModal;
AvnMenu* _menu;
double _lastScaling;
}
@@ -1164,6 +1331,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
+- (void)performClose:(id)sender
+{
+ if([[self delegate] respondsToSelector:@selector(windowShouldClose:)])
+ {
+ if(![[self delegate] windowShouldClose:self]) return;
+ }
+ else if([self respondsToSelector:@selector(windowShouldClose:)])
+ {
+ if(![self windowShouldClose:self]) return;
+ }
+
+ [self close];
+}
+
- (void)pollModalSession:(nonnull NSModalSession)session
{
auto response = [NSApp runModalSession:session];
@@ -1322,11 +1503,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto ch = objc_cast(uch);
if(ch == nil)
continue;
+
+ if(![ch isModal])
+ continue;
+
return FALSE;
}
return TRUE;
}
+-(bool) isModal
+{
+ return _isModal;
+}
+
+-(void) setModal: (bool) isModal
+{
+ _isModal = isModal;
+}
+
-(void)makeKeyWindow
{
if([self activateAppropriateChild: true])
@@ -1378,7 +1573,66 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)windowDidResize:(NSNotification *)notification
{
- _parent->OnResized();
+ auto parent = dynamic_cast(_parent.operator->());
+
+ if(parent != nullptr)
+ {
+ parent->WindowStateChanged();
+ }
+}
+
+- (void)windowWillExitFullScreen:(NSNotification *)notification
+{
+ auto parent = dynamic_cast(_parent.operator->());
+
+ if(parent != nullptr)
+ {
+ parent->StartStateTransition();
+ }
+}
+
+- (void)windowDidExitFullScreen:(NSNotification *)notification
+{
+ auto parent = dynamic_cast(_parent.operator->());
+
+ if(parent != nullptr)
+ {
+ parent->EndStateTransition();
+
+ if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized)
+ {
+ NSRect screenRect = [[self screen] visibleFrame];
+ [self setFrame:screenRect display:YES];
+ }
+
+ if(parent->WindowState() == Minimized)
+ {
+ [self miniaturize:nullptr];
+ }
+
+ parent->WindowStateChanged();
+ }
+}
+
+- (void)windowWillEnterFullScreen:(NSNotification *)notification
+{
+ auto parent = dynamic_cast(_parent.operator->());
+
+ if(parent != nullptr)
+ {
+ parent->StartStateTransition();
+ }
+}
+
+- (void)windowDidEnterFullScreen:(NSNotification *)notification
+{
+ auto parent = dynamic_cast(_parent.operator->());
+
+ if(parent != nullptr)
+ {
+ parent->EndStateTransition();
+ parent->WindowStateChanged();
+ }
}
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index f3f70719e3..e02308b5c6 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -59,8 +59,8 @@
-
-
+
+
No Decorations
Border Only
Full Decorations
@@ -69,6 +69,7 @@
Light
Dark
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index bea751ad4c..935db20757 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -7,7 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views"
- x:Class="ControlCatalog.MainWindow">
+ x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
index b6aa3e92cd..0257b4ce66 100644
--- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
@@ -1,4 +1,5 @@
using System.Reactive;
+using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
@@ -11,6 +12,8 @@ namespace ControlCatalog.ViewModels
private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true;
+ private WindowState _windowState;
+ private WindowState[] _windowStates;
public MainWindowViewModel(IManagedNotificationManager notificationManager)
{
@@ -45,10 +48,32 @@ namespace ControlCatalog.ViewModels
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
});
- ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
+ ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
{
IsMenuItemChecked = !IsMenuItemChecked;
});
+
+ WindowState = WindowState.Normal;
+
+ WindowStates = new WindowState[]
+ {
+ WindowState.Minimized,
+ WindowState.Normal,
+ WindowState.Maximized,
+ WindowState.FullScreen,
+ };
+ }
+
+ public WindowState WindowState
+ {
+ get { return _windowState; }
+ set { this.RaiseAndSetIfChanged(ref _windowState, value); }
+ }
+
+ public WindowState[] WindowStates
+ {
+ get { return _windowStates; }
+ set { this.RaiseAndSetIfChanged(ref _windowStates, value); }
}
public IManagedNotificationManager NotificationManager
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index 1ce9f5db06..ca1d97290e 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -254,7 +254,7 @@ namespace Avalonia.Animation
cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
}
- var newKF = new AnimatorKeyFrame(handler, cue);
+ var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
subscriptions.Add(newKF.BindSetter(setter, control));
diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs
index 36d15e518e..f6a0c12be4 100644
--- a/src/Avalonia.Animation/AnimatorKeyFrame.cs
+++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs
@@ -24,11 +24,20 @@ namespace Avalonia.Animation
{
AnimatorType = animatorType;
Cue = cue;
+ KeySpline = null;
+ }
+
+ public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
+ {
+ AnimatorType = animatorType;
+ Cue = cue;
+ KeySpline = keySpline;
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Cue Cue { get; }
+ public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;
diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs
index aa5e6aaf14..0660440e30 100644
--- a/src/Avalonia.Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Animation/Animators/Animator`1.cs
@@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators
else
newValue = (T)lastKeyframe.Value;
+ if (lastKeyframe.KeySpline != null)
+ progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
+
return Interpolate(progress, oldValue, newValue);
}
diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs
index ec59586584..c2cc1aa051 100644
--- a/src/Avalonia.Animation/KeyFrame.cs
+++ b/src/Avalonia.Animation/KeyFrame.cs
@@ -19,6 +19,7 @@ namespace Avalonia.Animation
{
private TimeSpan _ktimeSpan;
private Cue _kCue;
+ private KeySpline _kKeySpline;
public KeyFrame()
{
@@ -74,6 +75,25 @@ namespace Avalonia.Animation
}
}
+ ///
+ /// Gets or sets the KeySpline of this .
+ ///
+ /// The key spline.
+ public KeySpline KeySpline
+ {
+ get
+ {
+ return _kKeySpline;
+ }
+ set
+ {
+ _kKeySpline = value;
+ if (value != null && !value.IsValid())
+ {
+ throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0.");
+ }
+ }
+ }
}
diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs
new file mode 100644
index 0000000000..5a4f7a15a3
--- /dev/null
+++ b/src/Avalonia.Animation/KeySpline.cs
@@ -0,0 +1,349 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Text;
+using Avalonia;
+using Avalonia.Utilities;
+
+// Ported from WPF open-source code.
+// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Determines how an animation is used based on a cubic bezier curve.
+ /// X1 and X2 must be between 0.0 and 1.0, inclusive.
+ /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
+ ///
+ [TypeConverter(typeof(KeySplineTypeConverter))]
+ public class KeySpline : AvaloniaObject
+ {
+ // Control points
+ private double _controlPointX1;
+ private double _controlPointY1;
+ private double _controlPointX2;
+ private double _controlPointY2;
+ private bool _isSpecified;
+ private bool _isDirty;
+
+ // The parameter that corresponds to the most recent time
+ private double _parameter;
+
+ // Cached coefficients
+ private double _Bx; // 3*points[0].X
+ private double _Cx; // 3*points[1].X
+ private double _Cx_Bx; // 2*(Cx - Bx)
+ private double _three_Cx; // 3 - Cx
+
+ private double _By; // 3*points[0].Y
+ private double _Cy; // 3*points[1].Y
+
+ // constants
+ private const double _accuracy = .001; // 1/3 the desired accuracy in X
+ private const double _fuzz = .000001; // computational zero
+
+ ///
+ /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1.
+ ///
+ public KeySpline()
+ {
+ _controlPointX1 = 0.0;
+ _controlPointY1 = 0.0;
+ _controlPointX2 = 1.0;
+ _controlPointY2 = 1.0;
+ _isDirty = true;
+ }
+
+ ///
+ /// Create a with the given parameters
+ ///
+ /// X coordinate for the first control point
+ /// Y coordinate for the first control point
+ /// X coordinate for the second control point
+ /// Y coordinate for the second control point
+ public KeySpline(double x1, double y1, double x2, double y2)
+ {
+ _controlPointX1 = x1;
+ _controlPointY1 = y1;
+ _controlPointX2 = x2;
+ _controlPointY2 = y2;
+ _isDirty = true;
+ }
+
+ ///
+ /// Parse a from a string. The string
+ /// needs to contain 4 values in it for the 2 control points.
+ ///
+ /// string with 4 values in it
+ /// culture of the string
+ /// Thrown if the string does not have 4 values
+ /// A with the appropriate values set
+ public static KeySpline Parse(string value, CultureInfo culture)
+ {
+ using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
+ {
+ return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
+ }
+ }
+
+ ///
+ /// X coordinate of the first control point
+ ///
+ public double ControlPointX1
+ {
+ get => _controlPointX1;
+ set
+ {
+ if (IsValidXValue(value))
+ {
+ _controlPointX1 = value;
+ }
+ else
+ {
+ throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
+ }
+ }
+ }
+
+ ///
+ /// Y coordinate of the first control point
+ ///
+ public double ControlPointY1
+ {
+ get => _controlPointY1;
+ set => _controlPointY1 = value;
+ }
+
+ ///
+ /// X coordinate of the second control point
+ ///
+ public double ControlPointX2
+ {
+ get => _controlPointX2;
+ set
+ {
+ if (IsValidXValue(value))
+ {
+ _controlPointX2 = value;
+ }
+ else
+ {
+ throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
+ }
+ }
+ }
+
+ ///
+ /// Y coordinate of the second control point
+ ///
+ public double ControlPointY2
+ {
+ get => _controlPointY2;
+ set => _controlPointY2 = value;
+ }
+
+ ///
+ /// Calculates spline progress from a linear progress.
+ ///
+ /// the linear progress
+ /// the spline progress
+ public double GetSplineProgress(double linearProgress)
+ {
+ if (_isDirty)
+ {
+ Build();
+ }
+
+ if (!_isSpecified)
+ {
+ return linearProgress;
+ }
+ else
+ {
+ SetParameterFromX(linearProgress);
+
+ return GetBezierValue(_By, _Cy, _parameter);
+ }
+ }
+
+ ///
+ /// Check to see whether the is valid by looking
+ /// at its X values.
+ ///
+ /// true if the X values for this fall in
+ /// acceptable range; false otherwise.
+ public bool IsValid()
+ {
+ return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ private bool IsValidXValue(double value)
+ {
+ return value >= 0.0 && value <= 1.0;
+ }
+
+ ///
+ /// Compute cached coefficients.
+ ///
+ private void Build()
+ {
+ if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1)
+ {
+ // This KeySpline would have no effect on the progress.
+ _isSpecified = false;
+ }
+ else
+ {
+ _isSpecified = true;
+
+ _parameter = 0;
+
+ // X coefficients
+ _Bx = 3 * _controlPointX1;
+ _Cx = 3 * _controlPointX2;
+ _Cx_Bx = 2 * (_Cx - _Bx);
+ _three_Cx = 3 - _Cx;
+
+ // Y coefficients
+ _By = 3 * _controlPointY1;
+ _Cy = 3 * _controlPointY2;
+ }
+
+ _isDirty = false;
+ }
+
+ ///
+ /// Get an X or Y value with the Bezier formula.
+ ///
+ /// the second Bezier coefficient
+ /// the third Bezier coefficient
+ /// the parameter value to evaluate at
+ /// the value of the Bezier function at the given parameter
+ static private double GetBezierValue(double b, double c, double t)
+ {
+ double s = 1.0 - t;
+ double t2 = t * t;
+
+ return b * t * s * s + c * t2 * s + t2 * t;
+ }
+
+ ///
+ /// Get X and dX/dt at a given parameter
+ ///
+ /// the parameter value to evaluate at
+ /// the value of x there
+ /// the value of dx/dt there
+ private void GetXAndDx(double t, out double x, out double dx)
+ {
+ double s = 1.0 - t;
+ double t2 = t * t;
+ double s2 = s * s;
+
+ x = _Bx * t * s2 + _Cx * t2 * s + t2 * t;
+ dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2;
+ }
+
+ ///
+ /// Compute the parameter value that corresponds to a given X value, using a modified
+ /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make
+ /// use of some known properties of this particular function:
+ /// * We are only interested in solutions in the interval [0,1]
+ /// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use
+ /// that to clamp down the search interval with every probe.
+ /// * The derivative of X and Y are between 0 and 3.
+ ///
+ /// the time, scaled to fit in [0,1]
+ private void SetParameterFromX(double time)
+ {
+ // Dynamic search interval to clamp with
+ double bottom = 0;
+ double top = 1;
+
+ if (time == 0)
+ {
+ _parameter = 0;
+ }
+ else if (time == 1)
+ {
+ _parameter = 1;
+ }
+ else
+ {
+ // Loop while improving the guess
+ while (top - bottom > _fuzz)
+ {
+ double x, dx, absdx;
+
+ // Get x and dx/dt at the current parameter
+ GetXAndDx(_parameter, out x, out dx);
+ absdx = Math.Abs(dx);
+
+ // Clamp down the search interval, relying on the monotonicity of X(t)
+ if (x > time)
+ {
+ top = _parameter; // because parameter > solution
+ }
+ else
+ {
+ bottom = _parameter; // because parameter < solution
+ }
+
+ // The desired accuracy is in ultimately in y, not in x, so the
+ // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
+ // But dy/dt <=3, so we omit that
+ if (Math.Abs(x - time) < _accuracy * absdx)
+ {
+ break; // We're there
+ }
+
+ if (absdx > _fuzz)
+ {
+ // Nonzero derivative, use Newton-Raphson to obtain the next guess
+ double next = _parameter - (x - time) / dx;
+
+ // If next guess is out of the search interval then clamp it in
+ if (next >= top)
+ {
+ _parameter = (_parameter + top) / 2;
+ }
+ else if (next <= bottom)
+ {
+ _parameter = (_parameter + bottom) / 2;
+ }
+ else
+ {
+ // Next guess is inside the search interval, accept it
+ _parameter = next;
+ }
+ }
+ else // Zero derivative, halve the search interval
+ {
+ _parameter = (bottom + top) / 2;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Converts string values to values
+ ///
+ public class KeySplineTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ return KeySpline.Parse((string)value, culture);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index 18c4c88aa5..7076c97717 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -232,6 +232,23 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
+ ///
+ /// Called when the ComboBox popup is closed, with the
+ /// that caused the popup to close.
+ ///
+ /// The event args.
+ ///
+ /// This method can be overridden to control whether the event that caused the popup to close
+ /// is swallowed or passed through.
+ ///
+ protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
+ {
+ if (e.CloseEvent is PointerEventArgs pointerEvent)
+ {
+ pointerEvent.Handled = true;
+ }
+ }
+
internal void ItemFocused(ComboBoxItem dropDownItem)
{
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@@ -245,10 +262,7 @@ namespace Avalonia.Controls
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
+ PopupClosedOverride(e);
if (CanFocus(this))
{
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 3715bc52a4..8ad622ba4a 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -392,7 +392,7 @@ namespace Avalonia.Controls.Platform
{
var control = e.Source as ILogical;
- if (!Menu.IsLogicalParentOf(control))
+ if (!Menu.IsLogicalAncestorOf(control))
{
Menu.Close();
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 905a14cfee..9cbde72f7f 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
- AffectsRender(PasswordCharProperty,
- SelectionBrushProperty, SelectionForegroundBrushProperty,
- SelectionStartProperty, SelectionEndProperty);
+ AffectsRender(SelectionBrushProperty);
- Observable.Merge(
- TextProperty.Changed,
- SelectionStartProperty.Changed,
- SelectionEndProperty.Changed,
- PasswordCharProperty.Changed
- ).AddClassHandler((x,_) => x.InvalidateFormattedText());
+ Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
+ TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
+ TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
+ TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
+ SelectionStartProperty.Changed, SelectionEndProperty.Changed,
+ SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
+ ).AddClassHandler((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue));
}
@@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters
{
get
{
- return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
+ return _formattedText ?? (_formattedText = CreateFormattedText());
}
}
@@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
-
+
public IBrush CaretBrush
{
get => GetValue(CaretBrushProperty);
@@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters
///
protected void InvalidateFormattedText()
{
- if (_formattedText != null)
- {
- _constraint = _formattedText.Constraint;
- _formattedText = null;
- }
+ _formattedText = null;
- InvalidateVisual();
+ InvalidateMeasure();
}
///
@@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters
}
FormattedText.Constraint = Bounds.Size;
+
context.DrawText(Foreground, new Point(), FormattedText);
}
@@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters
///
/// Creates the used to render the text.
///
- /// The constraint of the text.
- /// The text to generated the for.
/// A object.
- protected virtual FormattedText CreateFormattedText(Size constraint, string text)
+ protected virtual FormattedText CreateFormattedText()
{
FormattedText result = null;
+ var text = Text;
+
if (PasswordChar != default(char))
{
- result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
+ result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}
else
{
- result = CreateFormattedTextInternal(constraint, text);
+ result = CreateFormattedTextInternal(_constraint, text);
}
var selectionStart = SelectionStart;
@@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters
{
if (TextWrapping == TextWrapping.Wrap)
{
- FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
+ _constraint = new Size(availableSize.Width, double.PositiveInfinity);
}
else
{
- FormattedText.Constraint = Size.Infinity;
+ _constraint = Size.Infinity;
}
+ _formattedText = null;
+
return FormattedText.Bounds.Size;
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index dd00b850fe..75f32c862e 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -500,6 +500,19 @@ namespace Avalonia.Controls
EnsureInitialized();
IsVisible = true;
+
+ var initialSize = new Size(
+ double.IsNaN(Width) ? ClientSize.Width : Width,
+ double.IsNaN(Height) ? ClientSize.Height : Height);
+
+ if (initialSize != ClientSize)
+ {
+ using (BeginAutoSizing())
+ {
+ PlatformImpl?.Resize(initialSize);
+ }
+ }
+
LayoutManager.ExecuteInitialLayoutPass(this);
var result = new TaskCompletionSource();
diff --git a/src/Avalonia.Controls/WindowState.cs b/src/Avalonia.Controls/WindowState.cs
index 4ed30e726e..777b52dc11 100644
--- a/src/Avalonia.Controls/WindowState.cs
+++ b/src/Avalonia.Controls/WindowState.cs
@@ -19,5 +19,10 @@ namespace Avalonia.Controls
/// The window is maximized.
///
Maximized,
+
+ ///
+ /// The window is fullscreen.
+ ///
+ FullScreen,
}
}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
index b967b40c0d..616f260397 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
@@ -34,7 +34,7 @@ namespace Avalonia.Dialogs
return;
}
- var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control);
+ var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
if (e.ClickCount == 2 || isQuickLink)
{
if (model.ItemType == ManagedFileChooserItemType.File)
diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs
index 6d6b2c5296..ec010815f4 100644
--- a/src/Avalonia.Native/WindowImpl.cs
+++ b/src/Avalonia.Native/WindowImpl.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Native
public void SetSystemDecorations(Controls.SystemDecorations enabled)
{
- _native.HasDecorations = (Interop.SystemDecorations)enabled;
+ _native.Decorations = (Interop.SystemDecorations)enabled;
}
public void SetTitleBarColor (Avalonia.Media.Color color)
diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
index 1003375978..458ab0fce2 100644
--- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
+++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
@@ -1,11 +1,18 @@
using System;
using System.Collections.Generic;
-using System.Linq;
namespace Avalonia.LogicalTree
{
+ ///
+ /// Provides extension methods for working with the logical tree.
+ ///
public static class LogicalExtensions
{
+ ///
+ /// Enumerates the ancestors of an in the logical tree.
+ ///
+ /// The logical.
+ /// The logical's ancestors.
public static IEnumerable GetLogicalAncestors(this ILogical logical)
{
Contract.Requires(logical != null);
@@ -19,6 +26,11 @@ namespace Avalonia.LogicalTree
}
}
+ ///
+ /// Enumerates an and its ancestors in the logical tree.
+ ///
+ /// The logical.
+ /// The logical and its ancestors.
public static IEnumerable GetSelfAndLogicalAncestors(this ILogical logical)
{
yield return logical;
@@ -29,11 +41,50 @@ namespace Avalonia.LogicalTree
}
}
+ ///
+ /// Finds first ancestor of given type.
+ ///
+ /// Ancestor type.
+ /// The logical.
+ /// If given logical should be included in search.
+ /// First ancestor of given type.
+ public static T FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class
+ {
+ if (logical is null)
+ {
+ return null;
+ }
+
+ ILogical parent = includeSelf ? logical : logical.LogicalParent;
+
+ while (parent != null)
+ {
+ if (parent is T result)
+ {
+ return result;
+ }
+
+ parent = parent.LogicalParent;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Enumerates the children of an in the logical tree.
+ ///
+ /// The logical.
+ /// The logical children.
public static IEnumerable GetLogicalChildren(this ILogical logical)
{
return logical.LogicalChildren;
}
+ ///
+ /// Enumerates the descendants of an in the logical tree.
+ ///
+ /// The logical.
+ /// The logical's ancestors.
public static IEnumerable GetLogicalDescendants(this ILogical logical)
{
foreach (ILogical child in logical.LogicalChildren)
@@ -47,6 +98,11 @@ namespace Avalonia.LogicalTree
}
}
+ ///
+ /// Enumerates an and its descendants in the logical tree.
+ ///
+ /// The logical.
+ /// The logical and its ancestors.
public static IEnumerable GetSelfAndLogicalDescendants(this ILogical logical)
{
yield return logical;
@@ -57,16 +113,56 @@ namespace Avalonia.LogicalTree
}
}
+ ///
+ /// Finds first descendant of given type.
+ ///
+ /// Descendant type.
+ /// The logical.
+ /// If given logical should be included in search.
+ /// First descendant of given type.
+ public static T FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class
+ {
+ if (logical is null)
+ {
+ return null;
+ }
+
+ if (includeSelf && logical is T result)
+ {
+ return result;
+ }
+
+ return FindDescendantOfTypeCore(logical);
+ }
+
+ ///
+ /// Gets the logical parent of an .
+ ///
+ /// The logical.
+ /// The parent, or null if the logical is unparented.
public static ILogical GetLogicalParent(this ILogical logical)
{
return logical.LogicalParent;
}
+ ///
+ /// Gets the logical parent of an .
+ ///
+ /// The type of the logical parent.
+ /// The logical.
+ ///
+ /// The parent, or null if the logical is unparented or its parent is not of type .
+ ///
public static T GetLogicalParent(this ILogical logical) where T : class
{
return logical.LogicalParent as T;
}
+ ///
+ /// Enumerates the siblings of an in the logical tree.
+ ///
+ /// The logical.
+ /// The logical siblings.
public static IEnumerable GetLogicalSiblings(this ILogical logical)
{
ILogical parent = logical.LogicalParent;
@@ -80,9 +176,55 @@ namespace Avalonia.LogicalTree
}
}
- public static bool IsLogicalParentOf(this ILogical logical, ILogical target)
+ ///
+ /// Tests whether an is an ancestor of another logical.
+ ///
+ /// The logical.
+ /// The potential descendant.
+ ///
+ /// True if is an ancestor of ;
+ /// otherwise false.
+ ///
+ public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
{
- return target.GetLogicalAncestors().Any(x => x == logical);
+ ILogical current = target?.LogicalParent;
+
+ while (current != null)
+ {
+ if (current == logical)
+ {
+ return true;
+ }
+
+ current = current.LogicalParent;
+ }
+
+ return false;
+ }
+
+ private static T FindDescendantOfTypeCore(ILogical logical) where T : class
+ {
+ var logicalChildren = logical.LogicalChildren;
+ var logicalChildrenCount = logicalChildren.Count;
+
+ for (var i = 0; i < logicalChildrenCount; i++)
+ {
+ ILogical child = logicalChildren[i];
+
+ if (child is T result)
+ {
+ return result;
+ }
+
+ var childResult = FindDescendantOfTypeCore(child);
+
+ if (!(childResult is null))
+ {
+ return childResult;
+ }
+ }
+
+ return null;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs
index 2de629432c..f9410afe6a 100644
--- a/src/Avalonia.Visuals/Media/FontManager.cs
+++ b/src/Avalonia.Visuals/Media/FontManager.cs
@@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
+ if (string.IsNullOrEmpty(DefaultFontFamilyName))
+ {
+ throw new InvalidOperationException("Default font family name can't be null or empty.");
+ }
+
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
}
@@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService();
- if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
+ if (fontManagerImpl == null)
+ throw new InvalidOperationException("No font manager implementation was registered.");
current = new FontManager(fontManagerImpl);
@@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily;
}
- var key = new FontKey(fontFamily, fontWeight, fontStyle);
+ var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface))
{
@@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
- return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
- _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
+ foreach (var cachedTypeface in _typefaceCache.Values)
+ {
+ // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
+ if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
+ && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
+ {
+ return cachedTypeface;
+ }
+ }
+
+ var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
+ _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
null;
+
+ return matchedTypeface;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
index a9ea322d76..96312a5466 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Linq;
using System.Text;
using Avalonia.Utilities;
@@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames));
}
- Names = familyNames.Split(',').Select(x => x.Trim()).ToArray();
+ Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0];
diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs
index 1f1e9b067d..a8d81648ba 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs
@@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
{
public readonly struct FontKey : IEquatable
{
- public readonly FontFamily FontFamily;
- public readonly FontStyle Style;
- public readonly FontWeight Weight;
-
- public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
+ public FontKey(string familyName, FontWeight weight, FontStyle style)
{
- FontFamily = fontFamily;
+ FamilyName = familyName;
Style = style;
Weight = weight;
}
+ public string FamilyName { get; }
+ public FontStyle Style { get; }
+ public FontWeight Weight { get; }
+
public override int GetHashCode()
{
- var hash = FontFamily.GetHashCode();
+ var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other)
{
- return FontFamily == other.FontFamily &&
+ return FamilyName == other.FamilyName &&
Style == other.Style &&
Weight == other.Weight;
}
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index 69806b22f2..53231ee1dd 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -200,7 +200,13 @@ namespace Avalonia.Media
private void Set(ref T field, T value)
{
+ if (field != null && field.Equals(value))
+ {
+ return;
+ }
+
field = value;
+
_platformImpl = null;
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
index 7956c5f260..7da39dc5dc 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
@@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback
currentTypeface =
- FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
+ FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
index 9c20efd867..2ff4952cab 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
@@ -2,7 +2,7 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
- internal ref struct CodepointEnumerator
+ public ref struct CodepointEnumerator
{
private ReadOnlySlice _text;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
deleted file mode 100644
index 3385116f26..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-namespace Avalonia.Media.TextFormatting.Unicode
-{
- public enum UnicodeGeneralCategory : byte
- {
- Other, //C# Cc | Cf | Cn | Co | Cs
- Control, //Cc
- Format, //Cf
- Unassigned, //Cn
- PrivateUse, //Co
- Surrogate, //Cs
- Letter, //L# Ll | Lm | Lo | Lt | Lu
- CasedLetter, //LC# Ll | Lt | Lu
- LowercaseLetter, //Ll
- ModifierLetter, //Lm
- OtherLetter, //Lo
- TitlecaseLetter, //Lt
- UppercaseLetter, //Lu
- Mark, //M
- SpacingMark, //Mc
- EnclosingMark, //Me
- NonspacingMark, //Mn
- Number, //N# Nd | Nl | No
- DecimalNumber, //Nd
- LetterNumber, //Nl
- OtherNumber, //No
- Punctuation, //P
- ConnectorPunctuation, //Pc
- DashPunctuation, //Pd
- ClosePunctuation, //Pe
- FinalPunctuation, //Pf
- InitialPunctuation, //Pi
- OtherPunctuation, //Po
- OpenPunctuation, //Ps
- Symbol, //S# Sc | Sk | Sm | So
- CurrencySymbol, //Sc
- ModifierSymbol, //Sk
- MathSymbol, //Sm
- OtherSymbol, //So
- Separator, //Z# Zl | Zp | Zs
- LineSeparator, //Zl
- ParagraphSeparator, //Zp
- SpaceSeparator, //Zs
- }
-}
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index d5e361ca0e..0e6dda1710 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -35,6 +35,7 @@ namespace Avalonia.Rendering
private IRef _currentDraw;
private readonly IDeferredRendererLock _lock;
private readonly object _sceneLock = new object();
+ private readonly Action _updateSceneIfNeededDelegate;
///
/// Initializes a new instance of the class.
@@ -49,7 +50,7 @@ namespace Avalonia.Rendering
IRenderLoop renderLoop,
ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null,
- IDeferredRendererLock rendererLock = null)
+ IDeferredRendererLock rendererLock = null) : base(true)
{
Contract.Requires(root != null);
@@ -59,6 +60,7 @@ namespace Avalonia.Rendering
Layers = new RenderLayers();
_renderLoop = renderLoop;
_lock = rendererLock ?? new ManagedDeferredRendererLock();
+ _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
}
///
@@ -73,7 +75,7 @@ namespace Avalonia.Rendering
public DeferredRenderer(
IVisual root,
IRenderTarget renderTarget,
- ISceneBuilder sceneBuilder = null)
+ ISceneBuilder sceneBuilder = null) : base(true)
{
Contract.Requires(root != null);
Contract.Requires(renderTarget != null);
@@ -83,6 +85,7 @@ namespace Avalonia.Rendering
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_lock = new ManagedDeferredRendererLock();
+ _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
}
///
@@ -261,7 +264,8 @@ namespace Avalonia.Rendering
try
{
var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context);
-
+ if (updated)
+ FpsTick();
using (scene)
{
if (scene?.Item != null)
@@ -318,17 +322,25 @@ namespace Avalonia.Rendering
_lastSceneId = scene.Generation;
+ var isUiThread = Dispatcher.UIThread.CheckAccess();
// We have consumed the previously available scene, but there might be some dirty
// rects since the last update. *If* we are on UI thread, we can force immediate scene
// rebuild before rendering anything on-screen
// We are calling the same method recursively here
- if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate)
+ if (!recursiveCall && isUiThread && NeedsUpdate)
{
UpdateScene();
var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true);
return (rs, true);
}
+ // We are rendering a new scene version, so it's highly likely
+ // that there is already a pending update for animations
+ // So we are scheduling an update call so UI thread could prepare a scene before
+ // the next render timer tick
+ if (!recursiveCall && !isUiThread)
+ Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render);
+
// Indicate that we have updated the layers
return (sceneRef.Clone(), true);
}
@@ -534,6 +546,12 @@ namespace Avalonia.Rendering
context = RenderTarget.CreateDrawingContext(this);
}
+ private void UpdateSceneIfNeeded()
+ {
+ if(NeedsUpdate)
+ UpdateScene();
+ }
+
private void UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index 1e7b5c2923..b37d5d660b 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -7,6 +7,7 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
+ private readonly bool _useManualFpsCounting;
private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
@@ -14,8 +15,9 @@ namespace Avalonia.Rendering
private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate;
- public RendererBase()
+ public RendererBase(bool useManualFpsCounting = false)
{
+ _useManualFpsCounting = useManualFpsCounting;
_fpsText = new FormattedText
{
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default),
@@ -23,12 +25,15 @@ namespace Avalonia.Rendering
};
}
+ protected void FpsTick() => _framesThisSecond++;
+
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
{
var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate;
- ++_framesThisSecond;
+ if (!_useManualFpsCounting)
+ ++_framesThisSecond;
if (elapsed.TotalSeconds > 1)
{
diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
index 4b3e757e7a..8c4004efdc 100644
--- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
+++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
@@ -377,7 +377,19 @@ namespace Avalonia.VisualTree
///
public static bool IsVisualAncestorOf(this IVisual visual, IVisual target)
{
- return target.GetVisualAncestors().Any(x => x == visual);
+ IVisual current = target?.VisualParent;
+
+ while (current != null)
+ {
+ if (current == visual)
+ {
+ return true;
+ }
+
+ current = current.VisualParent;
+ }
+
+ return false;
}
public static IEnumerable SortByZIndex(this IEnumerable elements)
diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs
index db74a32b99..523b65c115 100644
--- a/src/Avalonia.X11/X11Atoms.cs
+++ b/src/Avalonia.X11/X11Atoms.cs
@@ -156,6 +156,7 @@ namespace Avalonia.X11
public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE;
public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ;
public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT;
+ public readonly IntPtr _NET_WM_STATE_FULLSCREEN;
public readonly IntPtr _XEMBED;
public readonly IntPtr _XEMBED_INFO;
public readonly IntPtr _MOTIF_WM_HINTS;
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 3a919e2bc4..026a1457a3 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -220,16 +220,11 @@ namespace Avalonia.X11
var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
- if (_popup || _systemDecorations == SystemDecorations.None)
- {
+ if (_popup
+ || _systemDecorations == SystemDecorations.None)
decorations = 0;
- }
- else if (_systemDecorations == SystemDecorations.BorderOnly)
- {
- decorations = MotifDecorations.Border;
- }
- if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
+ if (!_canResize)
{
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@@ -252,7 +247,7 @@ namespace Avalonia.X11
var min = _minMaxSize.minSize;
var max = _minMaxSize.maxSize;
- if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
+ if (!_canResize)
max = min = _realSize;
if (preResize.HasValue)
@@ -552,12 +547,21 @@ namespace Avalonia.X11
else if (value == WindowState.Maximized)
{
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+ ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
}
+ else if (value == WindowState.FullScreen)
+ {
+ ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+ ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
+ ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
+ _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
+ }
else
{
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+ ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
}
@@ -585,6 +589,12 @@ namespace Avalonia.X11
break;
}
+ if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
+ {
+ state = WindowState.FullScreen;
+ break;
+ }
+
if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
{
@@ -810,7 +820,7 @@ namespace Avalonia.X11
public void SetSystemDecorations(SystemDecorations enabled)
{
- _systemDecorations = enabled;
+ _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None;
UpdateMotifHints();
UpdateSizeHints(null);
}
@@ -1052,7 +1062,7 @@ namespace Avalonia.X11
void ChangeWMAtoms(bool enable, params IntPtr[] atoms)
{
- if (atoms.Length < 1 || atoms.Length > 4)
+ if (atoms.Length != 1 && atoms.Length != 2)
throw new ArgumentException();
if (!_mapped)
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 4bec7b3f56..9f99ed3cef 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -30,6 +30,10 @@ namespace Avalonia.Skia
private Matrix _currentTransform;
private GRContext _grContext;
private bool _disposed;
+
+ private readonly SKPaint _strokePaint = new SKPaint();
+ private readonly SKPaint _fillPaint = new SKPaint();
+
///
/// Context create info.
///
@@ -153,7 +157,7 @@ namespace Avalonia.Skia
///
public void DrawLine(IPen pen, Point p1, Point p2)
{
- using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
+ using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
}
@@ -165,8 +169,8 @@ namespace Avalonia.Skia
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
- using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
- using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper))
+ using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
+ using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null)
{
- using (var paint = CreatePaint(brush, rect.Size))
+ using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{
if (isRounded)
{
@@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
- using (var paint = CreatePaint(pen, rect.Size))
+ using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{
if (isRounded)
{
@@ -222,7 +226,7 @@ namespace Avalonia.Skia
///
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
- using (var paint = CreatePaint(foreground, text.Bounds.Size))
+ using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@@ -232,14 +236,14 @@ namespace Avalonia.Skia
///
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
- using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
+ using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
- paint.ApplyTo(glyphRunImpl.Paint);
+ ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
- (float)baselineOrigin.Y, glyphRunImpl.Paint);
+ (float)baselineOrigin.Y, paintWrapper.Paint);
}
}
@@ -323,7 +327,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
- _maskStack.Push(CreatePaint(mask, bounds.Size));
+ _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
}
///
@@ -364,6 +368,15 @@ namespace Avalonia.Skia
}
}
+ internal void ConfigureTextRendering(PaintWrapper wrapper)
+ {
+ var paint = wrapper.Paint;
+
+ paint.IsEmbeddedBitmapText = true;
+ paint.SubpixelText = true;
+ paint.LcdRenderText = _canTextUseLcdRendering;
+ }
+
///
/// Configure paint wrapper for using gradient brush.
///
@@ -514,17 +527,16 @@ namespace Avalonia.Skia
///
/// Creates paint wrapper for given brush.
///
+ /// The paint to wrap.
/// Source brush.
/// Target size.
+ /// Optional dispose of the supplied paint.
/// Paint wrapper for given brush.
- internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+ internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
{
- var paint = new SKPaint
- {
- IsAntialias = true
- };
+ var paintWrapper = new PaintWrapper(paint, disposePaint);
- var paintWrapper = new PaintWrapper(paint);
+ paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity;
@@ -572,10 +584,12 @@ namespace Avalonia.Skia
///
/// Creates paint wrapper for given pen.
///
+ /// The paint to wrap.
/// Source pen.
/// Target size.
+ /// Optional dispose of the supplied paint.
///
- private PaintWrapper CreatePaint(IPen pen, Size targetSize)
+ private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default;
}
- var rv = CreatePaint(pen.Brush, targetSize);
- var paint = rv.Paint;
+ var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;
@@ -668,7 +681,7 @@ namespace Avalonia.Skia
///
/// Skia cached paint state.
///
- private struct PaintState : IDisposable
+ private readonly struct PaintState : IDisposable
{
private readonly SKColor _color;
private readonly SKShader _shader;
@@ -696,14 +709,16 @@ namespace Avalonia.Skia
{
//We are saving memory allocations there
public readonly SKPaint Paint;
+ private readonly bool _disposePaint;
private IDisposable _disposable1;
private IDisposable _disposable2;
private IDisposable _disposable3;
- public PaintWrapper(SKPaint paint)
+ public PaintWrapper(SKPaint paint, bool disposePaint)
{
Paint = paint;
+ _disposePaint = disposePaint;
_disposable1 = null;
_disposable2 = null;
@@ -751,7 +766,15 @@ namespace Avalonia.Skia
///
public void Dispose()
{
- Paint?.Dispose();
+ if (_disposePaint)
+ {
+ Paint?.Dispose();
+ }
+ else
+ {
+ Paint?.Reset();
+ }
+
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 53aa6a147c..415a89e1c1 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -32,6 +32,27 @@ namespace Avalonia.Skia
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
+ SKFontStyle skFontStyle;
+
+ switch (fontWeight)
+ {
+ case FontWeight.Normal when fontStyle == FontStyle.Normal:
+ skFontStyle = SKFontStyle.Normal;
+ break;
+ case FontWeight.Normal when fontStyle == FontStyle.Italic:
+ skFontStyle = SKFontStyle.Italic;
+ break;
+ case FontWeight.Bold when fontStyle == FontStyle.Normal:
+ skFontStyle = SKFontStyle.Bold;
+ break;
+ case FontWeight.Bold when fontStyle == FontStyle.Italic:
+ skFontStyle = SKFontStyle.BoldItalic;
+ break;
+ default:
+ skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
+ break;
+ }
+
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
@@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
- if (fontFamily != null)
+ if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
{
- foreach (var familyName in fontFamily.FamilyNames)
+ var familyNames = fontFamily.FamilyNames;
+
+ for (var i = 1; i < familyNames.Count; i++)
{
- var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
- SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+ var skTypeface =
+ _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
- fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
+ fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
}
else
{
- var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
- SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+ var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
- fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
+ fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
@@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
- var skTypeface = SKTypeface.Default;
+ SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null)
{
@@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface);
}
+ if (skTypeface == null)
+ {
+ throw new InvalidOperationException(
+ $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+ }
+
return new GlyphTypefaceImpl(skTypeface);
}
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index d0157815a9..5f876464e2 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -149,7 +149,17 @@ namespace Avalonia.Skia
if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
- return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
+
+ var c = Text[Text.Length - 1];
+
+ switch (c)
+ {
+ case '\n':
+ case '\r':
+ return new Rect(r.X, r.Y, 0, _lineHeight);
+ default:
+ return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
+ }
}
return rects[index];
}
@@ -266,7 +276,8 @@ namespace Avalonia.Skia
if (fb != null)
{
//TODO: figure out how to get the brush size
- currentWrapper = context.CreatePaint(fb, new Size());
+ currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
+ new Size());
}
else
{
diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
index 7c3f718f9e..0fdea5ed40 100644
--- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
@@ -7,17 +7,11 @@ namespace Avalonia.Skia
///
public class GlyphRunImpl : IGlyphRunImpl
{
- public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
+ public GlyphRunImpl(SKTextBlob textBlob)
{
- Paint = paint;
TextBlob = textBlob;
}
- ///
- /// Gets the paint to draw with.
- ///
- public SKPaint Paint { get; }
-
///
/// Gets the text blob to draw.
///
@@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose()
{
TextBlob.Dispose();
- Paint.Dispose();
}
}
}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index f1df6a804e..d277267d5d 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Skia
private GRContext GrContext { get; }
- public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
+ public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu, long maxResourceBytes = 100663296)
{
if (customSkiaGpu != null)
{
@@ -26,6 +26,10 @@ namespace Avalonia.Skia
GrContext = _customSkiaGpu.GrContext;
+ GrContext.GetResourceCacheLimits(out var maxResources, out _);
+
+ GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
+
return;
}
@@ -39,6 +43,10 @@ namespace Avalonia.Skia
: GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)))
{
GrContext = GRContext.Create(GRBackend.OpenGL, iface);
+
+ GrContext.GetResourceCacheLimits(out var maxResources, out _);
+
+ GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
}
display.ClearContext();
}
@@ -149,6 +157,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
+ private static readonly SKPaint s_paint = new SKPaint
+ {
+ TextEncoding = SKTextEncoding.GlyphId,
+ IsAntialias = true,
+ IsStroke = false,
+ SubpixelText = true
+ };
+
+ private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
+
///
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
@@ -158,92 +176,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface;
- var paint = new SKPaint
- {
- TextSize = (float)glyphRun.FontRenderingEmSize,
- Typeface = typeface,
- TextEncoding = SKTextEncoding.GlyphId,
- IsAntialias = true,
- IsStroke = false,
- SubpixelText = true
- };
+ s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
+ s_paint.Typeface = typeface;
- using (var textBlobBuilder = new SKTextBlobBuilder())
- {
- SKTextBlob textBlob;
- width = 0;
+ SKTextBlob textBlob;
- var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
+ width = 0;
- if (glyphRun.GlyphOffsets.IsEmpty)
- {
- if (glyphTypeface.IsFixedPitch)
- {
- textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
-
- textBlob = textBlobBuilder.Build();
-
- width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
- }
- else
- {
- var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
-
- var positions = buffer.GetPositionSpan();
+ var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
- for (var i = 0; i < count; i++)
- {
- positions[i] = (float)width;
-
- if (glyphRun.GlyphAdvances.IsEmpty)
- {
- width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
- }
- else
- {
- width += glyphRun.GlyphAdvances[i];
- }
- }
+ if (glyphRun.GlyphOffsets.IsEmpty)
+ {
+ if (glyphTypeface.IsFixedPitch)
+ {
+ s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
- buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+ textBlob = s_textBlobBuilder.Build();
- textBlob = textBlobBuilder.Build();
- }
+ width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
- var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
-
- var glyphPositions = buffer.GetPositionSpan();
+ var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
- var currentX = 0.0;
+ var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
- var glyphOffset = glyphRun.GlyphOffsets[i];
-
- glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+ positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
- currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
- currentX += glyphRun.GlyphAdvances[i];
+ width += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
- width = currentX;
+ textBlob = s_textBlobBuilder.Build();
+ }
+ }
+ else
+ {
+ var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
+
+ var glyphPositions = buffer.GetPositionSpan();
+
+ var currentX = 0.0;
+
+ for (var i = 0; i < count; i++)
+ {
+ var glyphOffset = glyphRun.GlyphOffsets[i];
- textBlob = textBlobBuilder.Build();
+ glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+
+ if (glyphRun.GlyphAdvances.IsEmpty)
+ {
+ currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ }
+ else
+ {
+ currentX += glyphRun.GlyphAdvances[i];
+ }
}
- return new GlyphRunImpl(paint, textBlob);
+ buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+
+ width = currentX;
+
+ textBlob = s_textBlobBuilder.Build();
}
+
+ return new GlyphRunImpl(textBlob);
+
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
index ce82835968..7aea90e61e 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
@@ -19,7 +19,7 @@ namespace Avalonia.Skia
public SKTypeface Get(Typeface typeface)
{
- var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style);
+ var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
return GetNearestMatch(_typefaces, key);
}
@@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0)
{
- return SKTypeface.Default;
+ return null;
}
key = keys[0];
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
index 4f04d25dee..a9aed80a04 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
@@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue;
}
- var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
+ var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
typeFaceCollection.AddTypeface(key, typeface);
}
diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs
index f4b8fe3c1d..2d5e5990e6 100644
--- a/src/Skia/Avalonia.Skia/SkiaOptions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs
@@ -8,9 +8,18 @@ namespace Avalonia
///
public class SkiaOptions
{
+ public SkiaOptions()
+ {
+ MaxGpuResourceSizeBytes = 100663296; // Value taken from skia.
+ }
///
/// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
///
public Func CustomGpuFactory { get; set; }
+
+ ///
+ /// The maximum number of bytes for video memory to store textures and resources.
+ ///
+ public long MaxGpuResourceSizeBytes { get; set; }
}
}
diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
index b4b340d24d..9a5725e06f 100644
--- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs
+++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Skia
public static void Initialize(SkiaOptions options)
{
var customGpu = options.CustomGpuFactory?.Invoke();
- var renderInterface = new PlatformRenderInterface(customGpu);
+ var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes);
AvaloniaLocator.CurrentMutable
.Bind().ToConstant(renderInterface)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
index 8ba46dcbce..253a373106 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
@@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
- fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+ fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
return true;
}
diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
new file mode 100644
index 0000000000..1b01ebbe7f
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Runtime.InteropServices;
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+
+namespace Avalonia.Win32.Interop
+{
+ internal class TaskBarList
+ {
+ private static IntPtr s_taskBarList;
+ private static HrInit s_hrInitDelegate;
+ private static MarkFullscreenWindow s_markFullscreenWindowDelegate;
+
+ ///
+ /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
+ ///
+ /// Fullscreen state.
+ public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen)
+ {
+ if (s_taskBarList == IntPtr.Zero)
+ {
+ Guid clsid = ShellIds.TaskBarList;
+ Guid iid = ShellIds.ITaskBarList2;
+
+ int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList);
+
+ if (s_taskBarList != IntPtr.Zero)
+ {
+ var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer();
+
+ if (s_hrInitDelegate is null)
+ {
+ s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit);
+ }
+
+ if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK)
+ {
+ s_taskBarList = IntPtr.Zero;
+ }
+ }
+ }
+
+ if (s_taskBarList != IntPtr.Zero)
+ {
+ var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer();
+
+ if (s_markFullscreenWindowDelegate is null)
+ {
+ s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow);
+ }
+
+ s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen);
+ }
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index cbfa1abfb7..5601ccbafe 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop
WS_SIZEFRAME = 0x40000,
WS_SYSMENU = 0x80000,
WS_TABSTOP = 0x10000,
+ WS_THICKFRAME = 0x40000,
WS_VISIBLE = 0x10000000,
WS_VSCROLL = 0x200000,
WS_EX_DLGMODALFRAME = 0x00000001,
@@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop
internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter);
-
+ [DllImport("ole32.dll", PreserveSig = true)]
+ internal static extern int CoCreateInstance(ref Guid clsid,
+ IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter);
+
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);
@@ -1642,6 +1646,8 @@ namespace Avalonia.Win32.Interop
public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B");
public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8");
public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE");
+ public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090");
+ public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf");
}
[ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
@@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop
[MarshalAs(UnmanagedType.LPWStr)]
public string pszSpec;
}
+
+ public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen);
+ public delegate HRESULT HrInit(IntPtr This);
+
+ public struct ITaskBarList2VTable
+ {
+ public IntPtr IUnknown1;
+ public IntPtr IUnknown2;
+ public IntPtr IUnknown3;
+ public IntPtr HrInit;
+ public IntPtr AddTab;
+ public IntPtr DeleteTab;
+ public IntPtr ActivateTab;
+ public IntPtr SetActiveAlt;
+ public IntPtr MarkFullscreenWindow;
+ }
}
[Flags]
diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs
index 963042b249..442794f0f0 100644
--- a/src/Windows/Avalonia.Win32/ScreenImpl.cs
+++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Win32
{
public class ScreenImpl : IScreenImpl
{
- public int ScreenCount
+ public int ScreenCount
{
get => GetSystemMetrics(SystemMetric.SM_CMONITORS);
}
@@ -33,7 +33,7 @@ namespace Avalonia.Win32
var shcore = LoadLibrary("shcore.dll");
var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
if (method != IntPtr.Zero)
- {
+ {
GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _);
dpi = (double)x;
}
@@ -51,11 +51,8 @@ namespace Avalonia.Win32
RECT bounds = monitorInfo.rcMonitor;
RECT workingArea = monitorInfo.rcWork;
- PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left,
- bounds.bottom - bounds.top);
- PixelRect avaloniaWorkArea =
- new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left,
- workingArea.bottom - workingArea.top);
+ PixelRect avaloniaBounds = bounds.ToPixelRect();
+ PixelRect avaloniaWorkArea = workingArea.ToPixelRect();
screens[index] =
new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1,
monitor);
diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs
index 8bdd4b7bfa..c6164e0868 100644
--- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs
+++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Win32
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
- UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
+ UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
var frm = (UnmanagedMethods.IFileDialog)unk;
var openDialog = dialog as OpenFileDialog;
@@ -105,9 +105,9 @@ namespace Avalonia.Win32
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
- Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
+ Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
- UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
+ UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
var frm = (UnmanagedMethods.IFileDialog)unk;
uint options;
frm.GetOptions(out options);
diff --git a/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
new file mode 100644
index 0000000000..8193611f6d
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
@@ -0,0 +1,13 @@
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+
+namespace Avalonia.Win32
+{
+ internal static class Win32TypeExtensions
+ {
+ public static PixelRect ToPixelRect(this RECT rect)
+ {
+ return new PixelRect(rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top);
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index e193c72ef7..d82e9f64f1 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -37,10 +37,14 @@ namespace Avalonia.Win32
{ WindowEdge.West, HitTestValues.HTLEFT }
};
+ private SavedWindowInfo _savedWindowInfo;
+ private bool _isFullScreenActive;
+
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif
+ private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
private readonly List _disabledBy;
private readonly TouchDevice _touchDevice;
private readonly MouseDevice _mouseDevice;
@@ -82,7 +86,9 @@ namespace Avalonia.Win32
_windowProperties = new WindowProperties
{
- ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full
+ ShowInTaskbar = false,
+ IsResizable = true,
+ Decorations = SystemDecorations.Full
};
_rendererLock = new ManagedDeferredRendererLock();
@@ -538,27 +544,98 @@ namespace Avalonia.Win32
}
}
+ ///
+ /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
+ /// Method must only be called from inside UpdateWindowProperties.
+ ///
+ ///
+ private void SetFullScreen(bool fullscreen)
+ {
+ if (fullscreen)
+ {
+ GetWindowRect(_hwnd, out var windowRect);
+ _savedWindowInfo.WindowRect = windowRect;
+
+ var current = GetStyle();
+ var currentEx = GetExtendedStyle();
+
+ _savedWindowInfo.Style = current;
+ _savedWindowInfo.ExStyle = currentEx;
+
+ // Set new window style and size.
+ SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false);
+ SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false);
+
+ // On expand, if we're given a window_rect, grow to it, otherwise do
+ // not resize.
+ MONITORINFO monitor_info = MONITORINFO.Create();
+ GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info);
+
+ var window_rect = monitor_info.rcMonitor.ToPixelRect();
+
+ SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
+ window_rect.Width, window_rect.Height,
+ SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+
+ _isFullScreenActive = true;
+ }
+ else
+ {
+ // Reset original window style and size. The multiple window size/moves
+ // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be
+ // repainted. Better-looking methods welcome.
+ _isFullScreenActive = false;
+
+ var windowStates = GetWindowStateStyles();
+ SetStyle((_savedWindowInfo.Style & ~WindowStateMask) | windowStates, false);
+ SetExtendedStyle(_savedWindowInfo.ExStyle, false);
+
+ // On restore, resize to the previous saved rect size.
+ var new_rect = _savedWindowInfo.WindowRect.ToPixelRect();
+
+ SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width,
+ new_rect.Height,
+ SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+
+ UpdateWindowProperties(_windowProperties, true);
+ }
+
+ TaskBarList.MarkFullscreen(_hwnd, fullscreen);
+ }
+
private void ShowWindow(WindowState state)
{
ShowWindowCommand command;
+ var newWindowProperties = _windowProperties;
+
switch (state)
{
case WindowState.Minimized:
+ newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Minimize;
break;
case WindowState.Maximized:
+ newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Maximize;
break;
case WindowState.Normal:
+ newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Restore;
break;
+ case WindowState.FullScreen:
+ newWindowProperties.IsFullScreen = true;
+ UpdateWindowProperties(newWindowProperties);
+ return;
+
default:
throw new ArgumentException("Invalid WindowState.");
}
+ UpdateWindowProperties(newWindowProperties);
+
UnmanagedMethods.ShowWindow(_hwnd, command);
if (state == WindowState.Maximized)
@@ -590,22 +667,69 @@ namespace Avalonia.Win32
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW);
}
}
+ }
+
+ private WindowStyles GetWindowStateStyles ()
+ {
+ return GetStyle() & WindowStateMask;
}
- private WindowStyles GetStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
+ private WindowStyles GetStyle()
+ {
+ if (_isFullScreenActive)
+ {
+ return _savedWindowInfo.Style;
+ }
+ else
+ {
+ return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
+ }
+ }
- private WindowStyles GetExtendedStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE);
+ private WindowStyles GetExtendedStyle()
+ {
+ if (_isFullScreenActive)
+ {
+ return _savedWindowInfo.ExStyle;
+ }
+ else
+ {
+ return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE);
+ }
+ }
- private void SetStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style);
+ private void SetStyle(WindowStyles style, bool save = true)
+ {
+ if (save)
+ {
+ _savedWindowInfo.Style = style;
+ }
- private void SetExtendedStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style);
+ if (!_isFullScreenActive)
+ {
+ SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style);
+ }
+ }
+
+ private void SetExtendedStyle(WindowStyles style, bool save = true)
+ {
+ if (save)
+ {
+ _savedWindowInfo.ExStyle = style;
+ }
+
+ if (!_isFullScreenActive)
+ {
+ SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style);
+ }
+ }
private void UpdateEnabled()
{
EnableWindow(_hwnd, _disabledBy.Count == 0);
}
- private void UpdateWindowProperties(WindowProperties newProperties)
+ private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false)
{
var oldProperties = _windowProperties;
@@ -613,7 +737,7 @@ namespace Avalonia.Win32
// according to the new values already.
_windowProperties = newProperties;
- if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar)
+ if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges)
{
var exStyle = GetExtendedStyle();
@@ -632,7 +756,7 @@ namespace Avalonia.Win32
// Otherwise it will still show in the taskbar.
}
- if (oldProperties.IsResizable != newProperties.IsResizable)
+ if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges)
{
var style = GetStyle();
@@ -648,7 +772,12 @@ namespace Avalonia.Win32
SetStyle(style);
}
- if (oldProperties.Decorations != newProperties.Decorations)
+ if (oldProperties.IsFullScreen != newProperties.IsFullScreen)
+ {
+ SetFullScreen(newProperties.IsFullScreen);
+ }
+
+ if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges)
{
var style = GetStyle();
@@ -663,30 +792,33 @@ namespace Avalonia.Win32
style &= ~fullDecorationFlags;
}
- var margins = new MARGINS
+ SetStyle(style);
+
+ if (!_isFullScreenActive)
{
- cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0
- };
+ var margins = new MARGINS
+ {
+ cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0
+ };
- DwmExtendFrameIntoClientArea(_hwnd, ref margins);
+ DwmExtendFrameIntoClientArea(_hwnd, ref margins);
- GetClientRect(_hwnd, out var oldClientRect);
- var oldClientRectOrigin = new POINT();
- ClientToScreen(_hwnd, ref oldClientRectOrigin);
- oldClientRect.Offset(oldClientRectOrigin);
+ GetClientRect(_hwnd, out var oldClientRect);
+ var oldClientRectOrigin = new POINT();
+ ClientToScreen(_hwnd, ref oldClientRectOrigin);
+ oldClientRect.Offset(oldClientRectOrigin);
- SetStyle(style);
+ var newRect = oldClientRect;
- var newRect = oldClientRect;
+ if (newProperties.Decorations == SystemDecorations.Full)
+ {
+ AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle());
+ }
- if (newProperties.Decorations == SystemDecorations.Full)
- {
- AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle());
+ SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
+ SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
+ SetWindowPosFlags.SWP_FRAMECHANGED);
}
-
- SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
- SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
- SetWindowPosFlags.SWP_FRAMECHANGED);
}
}
@@ -713,11 +845,19 @@ namespace Avalonia.Win32
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle;
+ private struct SavedWindowInfo
+ {
+ public WindowStyles Style { get; set; }
+ public WindowStyles ExStyle { get; set; }
+ public RECT WindowRect { get; set; }
+ };
+
private struct WindowProperties
{
public bool ShowInTaskbar;
public bool IsResizable;
public SystemDecorations Decorations;
+ public bool IsFullScreen;
}
}
}
diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
new file mode 100644
index 0000000000..df7c0693e1
--- /dev/null
+++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
@@ -0,0 +1,145 @@
+using System;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Xunit;
+
+namespace Avalonia.Animation.UnitTests
+{
+ public class KeySplineTests
+ {
+ [Theory]
+ [InlineData("1,2 3,4")]
+ [InlineData("1 2 3 4")]
+ [InlineData("1 2,3 4")]
+ [InlineData("1,2,3,4")]
+ public void Can_Parse_KeySpline_Via_TypeConverter(string input)
+ {
+ var conv = new KeySplineTypeConverter();
+
+ var keySpline = (KeySpline)conv.ConvertFrom(input);
+
+ Assert.Equal(1, keySpline.ControlPointX1);
+ Assert.Equal(2, keySpline.ControlPointY1);
+ Assert.Equal(3, keySpline.ControlPointX2);
+ Assert.Equal(4, keySpline.ControlPointY2);
+ }
+
+ [Theory]
+ [InlineData(0.00)]
+ [InlineData(0.50)]
+ [InlineData(1.00)]
+ public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input)
+ {
+ var keySpline = new KeySpline();
+ keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown
+ keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown
+ }
+
+ [Theory]
+ [InlineData(-0.01)]
+ [InlineData(1.01)]
+ public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input)
+ {
+ var keySpline = new KeySpline();
+ Assert.Throws(() => keySpline.ControlPointX1 = input);
+ Assert.Throws(() => keySpline.ControlPointX2 = input);
+ }
+
+ /*
+ To get the test values for the KeySpline test, you can:
+ 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
+ 2) Add the following xaml somewhere:
+
+
+
+
+ 3) Add the following code to the code behind:
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ CaptureData.Text += string.Format("\n{0} | {1}", myTranslateTransform3D.OffsetX, (TimeSpan)ExampleStoryboard.GetCurrentTime(this));
+ CaptureData.Text +=
+ "\nKeySpline=\"" + mySplineKeyFrame.KeySpline.ControlPoint1.X.ToString() + "," +
+ mySplineKeyFrame.KeySpline.ControlPoint1.Y.ToString() + " " +
+ mySplineKeyFrame.KeySpline.ControlPoint2.X.ToString() + "," +
+ mySplineKeyFrame.KeySpline.ControlPoint2.Y.ToString() + "\"";
+ CaptureData.Text += "\n-----";
+ }
+ 4) Run the app, mess with the slider values, then click the button to capture output values
+ **/
+
+ [Fact]
+ public void Check_KeySpline_Handled_properly()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(RotateTransform.AngleProperty, -2.5d),
+ },
+ KeyTime = TimeSpan.FromSeconds(0)
+ };
+
+ var keyframe2 = new KeyFrame()
+ {
+ Setters =
+ {
+ new Setter(RotateTransform.AngleProperty, 2.5d),
+ },
+ KeyTime = TimeSpan.FromSeconds(5),
+ KeySpline = new KeySpline(0.1123555056179775,
+ 0.657303370786517,
+ 0.8370786516853934,
+ 0.499999999999999999)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(5),
+ Children =
+ {
+ keyframe1,
+ keyframe2
+ },
+ IterationCount = new IterationCount(5),
+ PlaybackDirection = PlaybackDirection.Alternate
+ };
+
+ var rotateTransform = new RotateTransform(-2.5);
+ var rect = new Rectangle()
+ {
+ RenderTransform = rotateTransform
+ };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(rect, clock);
+
+ // position is what you'd expect at end and beginning
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(rotateTransform.Angle, -2.5);
+ clock.Step(TimeSpan.FromSeconds(5));
+ Assert.Equal(rotateTransform.Angle, 2.5);
+
+ // test some points in between end and beginning
+ var tolerance = 0.01;
+ clock.Step(TimeSpan.Parse("00:00:10.0153932"));
+ var expected = -2.4122350198982545;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:11.2655407"));
+ expected = -0.37153223002125113;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:12.6158773"));
+ expected = 0.3967885416786294;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+ clock.Step(TimeSpan.Parse("00:00:14.6495256"));
+ expected = 1.8016358493761722;
+ Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 29944c6f85..e99be9cfd2 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -336,210 +336,227 @@ namespace Avalonia.Controls.UnitTests
}
}
- [Fact]
- public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
+ public class SizingTests
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
{
- var child = new ChildControl();
- var target = new Window
- {
- Width = 100,
- Height = 50,
- SizeToContent = SizeToContent.Manual,
- Content = child
- };
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var child = new ChildControl();
+ var target = new Window
+ {
+ Width = 100,
+ Height = 50,
+ SizeToContent = SizeToContent.Manual,
+ Content = child
+ };
- target.Show();
+ Show(target);
- Assert.Equal(1, child.MeasureSizes.Count);
- Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
+ }
}
- }
- [Fact]
- public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
{
- var windowImpl = MockWindowingPlatform.CreateWindowMock();
- windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
-
- var child = new ChildControl();
- var target = new Window(windowImpl.Object)
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- SizeToContent = SizeToContent.Manual,
- Content = child
- };
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+ windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
- target.Show();
+ var child = new ChildControl();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ Content = child
+ };
- Assert.Equal(1, child.MeasureSizes.Count);
- Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
+ Show(target);
+
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
+ }
}
- }
- [Fact]
- public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
{
- var child = new ChildControl();
- var target = new Window
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Width = 100,
- Height = 50,
- SizeToContent = SizeToContent.WidthAndHeight,
- Content = child
- };
+ var child = new ChildControl();
+ var target = new Window
+ {
+ Width = 100,
+ Height = 50,
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child
+ };
- target.Show();
+ Show(target);
- Assert.Equal(1, child.MeasureSizes.Count);
- Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
+ }
}
- }
- [Fact]
- public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
- {
- // Issue #3784.
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
- var windowImpl = MockWindowingPlatform.CreateWindowMock();
- var clientSize = new Size(200, 200);
- var maxClientSize = new Size(480, 480);
-
- windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size =>
+ // Issue #3784.
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- clientSize = size.Constrain(maxClientSize);
- windowImpl.Object.Resized?.Invoke(clientSize);
- });
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+ var clientSize = new Size(200, 200);
+ var maxClientSize = new Size(480, 480);
- windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+ windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size =>
+ {
+ clientSize = size.Constrain(maxClientSize);
+ windowImpl.Object.Resized?.Invoke(clientSize);
+ });
- var child = new Canvas
- {
- Width = 400,
- Height = 800,
- };
- var target = new Window(windowImpl.Object)
- {
- SizeToContent = SizeToContent.WidthAndHeight,
- Content = child
- };
+ windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
- target.Show();
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 800,
+ };
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child
+ };
+
+ Show(target);
- Assert.Equal(new Size(400, 480), target.Bounds.Size);
+ Assert.Equal(new Size(400, 480), target.Bounds.Size);
- // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
- // parent control to be offset against.
- Assert.Equal(new Point(0, 0), target.Bounds.Position);
+ // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+ // parent control to be offset against.
+ Assert.Equal(new Point(0, 0), target.Bounds.Position);
+ }
}
- }
- [Fact]
- public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
{
- var child = new Canvas
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Width = 400,
- Height = 800,
- };
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 800,
+ };
- var target = new Window()
- {
- SizeToContent = SizeToContent.WidthAndHeight,
- Content = child
- };
+ var target = new Window()
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child
+ };
- target.Show();
+ Show(target);
- Assert.Equal(400, target.Width);
- Assert.Equal(800, target.Height);
+ Assert.Equal(400, target.Width);
+ Assert.Equal(800, target.Height);
+ }
}
- }
- [Fact]
- public void SizeToContent_Should_Not_Be_Lost_On_Show()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void SizeToContent_Should_Not_Be_Lost_On_Show()
{
- var child = new Canvas
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Width = 400,
- Height = 800,
- };
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 800,
+ };
- var target = new Window()
- {
- SizeToContent = SizeToContent.WidthAndHeight,
- Content = child
- };
+ var target = new Window()
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child
+ };
- target.Show();
+ Show(target);
- Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ }
}
- }
- [Fact]
- public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
{
- var child = new Canvas
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Width = 400,
- Height = 800,
- };
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 800,
+ };
- var target = new Window()
- {
- SizeToContent = SizeToContent.WidthAndHeight,
- Content = child
- };
+ var target = new Window()
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child
+ };
- target.Show();
+ Show(target);
- Assert.Equal(400, target.Width);
- Assert.Equal(800, target.Height);
+ Assert.Equal(400, target.Width);
+ Assert.Equal(800, target.Height);
- child.Width = 410;
- target.LayoutManager.ExecuteLayoutPass();
+ child.Width = 410;
+ target.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(410, target.Width);
- Assert.Equal(800, target.Height);
- Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ Assert.Equal(410, target.Width);
+ Assert.Equal(800, target.Height);
+ Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ }
}
- }
- [Fact]
- public void Setting_Width_Should_Resize_WindowImpl()
- {
- // Issue #3796
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ [Fact]
+ public void Setting_Width_Should_Resize_WindowImpl()
{
- var target = new Window()
+ // Issue #3796
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- Width = 400,
- Height = 800,
- };
+ var target = new Window()
+ {
+ Width = 400,
+ Height = 800,
+ };
- target.Show();
+ Show(target);
+
+ Assert.Equal(400, target.Width);
+ Assert.Equal(800, target.Height);
- Assert.Equal(400, target.Width);
- Assert.Equal(800, target.Height);
+ target.Width = 410;
+ target.LayoutManager.ExecuteLayoutPass();
- target.Width = 410;
- target.LayoutManager.ExecuteLayoutPass();
+ var windowImpl = Mock.Get(target.PlatformImpl);
+ windowImpl.Verify(x => x.Resize(new Size(410, 800)));
+ Assert.Equal(410, target.Width);
+ }
+ }
- var windowImpl = Mock.Get(target.PlatformImpl);
- windowImpl.Verify(x => x.Resize(new Size(410, 800)));
- Assert.Equal(410, target.Width);
+ protected virtual void Show(Window window)
+ {
+ window.Show();
+ }
+ }
+
+ public class DialogSizingTests : SizingTests
+ {
+ protected override void Show(Window window)
+ {
+ var owner = new Window();
+ window.ShowDialog(owner);
}
}
diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs
index 1e2443aff8..a0fbf704cf 100644
--- a/tests/Avalonia.RenderTests/TestBase.cs
+++ b/tests/Avalonia.RenderTests/TestBase.cs
@@ -184,7 +184,7 @@ namespace Avalonia.Direct2D1.RenderTests
public void Signal(DispatcherPriority prio)
{
- throw new NotImplementedException();
+ // No-op
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
index aef2423f8a..8d64190ebd 100644
--- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
+++ b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
@@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
public class CustomFontManagerImpl : IFontManagerImpl
{
private readonly Typeface[] _customTypefaces;
+ private readonly string _defaultFamilyName;
private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
- private readonly Typeface _italicTypeface =
+ private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
@@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
public CustomFontManagerImpl()
{
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
+ _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
}
public string GetDefaultFontFamilyName()
{
- return _defaultTypeface.FontFamily.ToString();
+ return _defaultFamilyName;
}
public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
@@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
return _customTypefaces.Select(x => x.FontFamily.Name);
}
+ private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
+
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey)
{
foreach (var customTypeface in _customTypefaces)
{
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
+ {
continue;
- fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle);
+ }
+
+ fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
return true;
}
- var fallback = SKFontManager.Default.MatchCharacter(codepoint);
+ var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
+ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
- fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle);
+ fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
return true;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
+ SKTypeface skTypeface;
+
switch (typeface.FontFamily.Name)
{
case "Twitter Color Emoji":
+ {
+ var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
+ skTypeface = typefaceCollection.Get(typeface);
+ break;
+ }
+
case "Noto Sans":
+ {
+ var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
+ skTypeface = typefaceCollection.Get(typeface);
+ break;
+ }
case "Noto Mono":
- var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
- var skTypeface = typefaceCollection.Get(typeface);
- return new GlyphTypefaceImpl(skTypeface);
+ {
+ var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
+ skTypeface = typefaceCollection.Get(typeface);
+ break;
+ }
default:
- return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name,
- (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style));
+ {
+ skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
+ (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+ break;
+ }
}
+
+ return new GlyphTypefaceImpl(skTypeface);
}
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
index dc2a40aeba..8f80d89ac6 100644
--- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
@@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
+
+ [Fact]
+ public void Should_Throw_For_Invalid_Custom_Font()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var fontManager = new FontManagerImpl();
+
+ Assert.Throws(() =>
+ fontManager.CreateGlyphTypeface(
+ new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
index 1f89a5833c..627b7c2ead 100644
--- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
@@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- maxWidth : 200,
+ maxWidth : 200,
maxHeight : 125,
textStyleOverrides: spans);
@@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests
}
}
+ private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
+
+ [Fact]
+ public void Should_Wrap()
+ {
+ using (Start())
+ {
+ for (var i = 0; i < 2000; i++)
+ {
+ var layout = new TextLayout(
+ Text,
+ Typeface.Default,
+ 12,
+ Brushes.Black,
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 50);
+ }
+ }
+ }
+
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
- .With(renderInterface: new PlatformRenderInterface(null),
+ .With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl : new CustomFontManagerImpl()));
diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
index affdc48f5e..55656fcfc0 100644
--- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
+++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
@@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
{
public class MockFontManagerImpl : IFontManagerImpl
{
+ private readonly string _defaultFamilyName;
+
+ public MockFontManagerImpl(string defaultFamilyName = "Default")
+ {
+ _defaultFamilyName = defaultFamilyName;
+ }
+
public string GetDefaultFontFamilyName()
{
- return "Default";
+ return _defaultFamilyName;
}
public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
- return new[] { "Default" };
+ return new[] { _defaultFamilyName };
}
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
index d35108080b..81a4ca6495 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System;
+using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
@@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
}
}
+
+ [Fact]
+ public void Should_Throw_When_Default_FamilyName_Is_Null()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
+ {
+ Assert.Throws(() => FontManager.Current);
+ }
+ }
}
}