Browse Source

Merge branch 'master' into prs/viewbox

pull/2066/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
dba58812b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/Avalonia.Native.v3.ncrunchproject
  2. 2
      native/Avalonia.Native/inc/avalonia-native.h
  3. 9
      native/Avalonia.Native/src/OSX/common.h
  4. 24
      native/Avalonia.Native/src/OSX/main.mm
  5. 2
      native/Avalonia.Native/src/OSX/window.h
  6. 125
      native/Avalonia.Native/src/OSX/window.mm
  7. 2
      samples/ControlCatalog.Desktop/Program.cs
  8. 4
      samples/ControlCatalog/MainView.xaml
  9. 5
      samples/ControlCatalog/Pages/DialogsPage.xaml
  10. 14
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  11. 2
      samples/ControlCatalog/Pages/MenuPage.xaml.cs
  12. 16
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  13. 124
      samples/ControlCatalog/Pages/TabControlPage.xaml
  14. 80
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  15. 111
      samples/ControlCatalog/SideBar.xaml
  16. 1
      scripts/ReplaceNugetCache.sh
  17. 12
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  18. 2
      src/Avalonia.Controls/ColumnDefinition.cs
  19. 57
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  20. 154
      src/Avalonia.Controls/Grid.cs
  21. 73
      src/Avalonia.Controls/GridSplitter.cs
  22. 2
      src/Avalonia.Controls/ItemsControl.cs
  23. 5
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  24. 21
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  25. 6
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  26. 19
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  27. 31
      src/Avalonia.Controls/SystemDialog.cs
  28. 164
      src/Avalonia.Controls/TabControl.cs
  29. 73
      src/Avalonia.Controls/TabItem.cs
  30. 6
      src/Avalonia.Controls/Templates/TemplateExtensions.cs
  31. 1
      src/Avalonia.Controls/TextBox.cs
  32. 23
      src/Avalonia.Controls/Utils/GridLayout.cs
  33. 651
      src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs
  34. 38
      src/Avalonia.Controls/Window.cs
  35. 5
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  36. 6
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  37. 3
      src/Avalonia.Input/InputExtensions.cs
  38. 6
      src/Avalonia.Layout/Properties/AssemblyInfo.cs
  39. 23
      src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs
  40. 108
      src/Avalonia.Native/DeferredRendererProxy.cs
  41. 4
      src/Avalonia.Native/GlPlatformFeature.cs
  42. 4
      src/Avalonia.Native/WindowImpl.cs
  43. 8
      src/Avalonia.Native/WindowImplBase.cs
  44. 44
      src/Avalonia.OpenGL/EglContext.cs
  45. 53
      src/Avalonia.OpenGL/EglDisplay.cs
  46. 4
      src/Avalonia.OpenGL/EglGlPlatformFeature.cs
  47. 32
      src/Avalonia.OpenGL/EglGlPlatformSurface.cs
  48. 28
      src/Avalonia.OpenGL/EglSurface.cs
  49. 4
      src/Avalonia.OpenGL/IGlContext.cs
  50. 10
      src/Avalonia.OpenGL/IGlSurface.cs
  51. 12
      src/Avalonia.Styling/StyledElement.cs
  52. 2
      src/Avalonia.Themes.Default/Accents/BaseDark.xaml
  53. 2
      src/Avalonia.Themes.Default/Accents/BaseLight.xaml
  54. 27
      src/Avalonia.Themes.Default/ButtonSpinner.xaml
  55. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  56. 29
      src/Avalonia.Themes.Default/NumericUpDown.xaml
  57. 22
      src/Avalonia.Themes.Default/ScrollBar.xaml
  58. 104
      src/Avalonia.Themes.Default/TabControl.xaml
  59. 39
      src/Avalonia.Themes.Default/TabItem.xaml
  60. 5
      src/Avalonia.Themes.Default/TextBox.xaml
  61. 177
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  62. 9
      src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs
  63. 17
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  64. 4
      src/Gtk/Avalonia.Gtk3/Interop/Native.cs
  65. 8
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  66. 13
      src/Gtk/Avalonia.Gtk3/WindowImpl.cs
  67. 2
      src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs
  68. 16
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs
  69. 8
      src/Shared/PlatformSupport/StandardRuntimePlatform.cs
  70. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  71. 7
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  72. 18
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  73. 9
      src/Windows/Avalonia.Win32/Win32Platform.cs
  74. 34
      src/Windows/Avalonia.Win32/WindowImpl.cs
  75. 14
      tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs
  76. 284
      tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs
  77. 145
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  78. 7
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  79. 4
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  80. 100
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs
  81. 58
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs
  82. 4
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs

5
.ncrunch/Avalonia.Native.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

2
native/Avalonia.Native/inc/avalonia-native.h

@ -213,7 +213,7 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{ {
virtual HRESULT ShowDialog (IUnknown**ppv) = 0; virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetHasDecorations(bool value) = 0; virtual HRESULT SetHasDecorations(bool value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0;

9
native/Avalonia.Native/src/OSX/common.h

@ -31,4 +31,13 @@ extern NSSize ToNSSize (AvnSize s);
#define NSDebugLog(...) (void)0 #define NSDebugLog(...) (void)0
#endif #endif
template<typename T> inline T* objc_cast(id from) {
if(from == nil)
return nil;
if ([from isKindOfClass:[T class]]) {
return static_cast<T*>(from);
}
return nil;
}
#endif #endif

24
native/Avalonia.Native/src/OSX/main.mm

@ -32,27 +32,23 @@ public:
- (void) do; - (void) do;
@end @end
@implementation ThreadingInitializer @implementation ThreadingInitializer
{
pthread_mutex_t mutex; int _fds[2];
pthread_cond_t cond; }
- (void) runOnce - (void) runOnce
{ {
pthread_mutex_lock(&mutex); char buf[]={0};
pthread_cond_signal(&cond); write(_fds[1], buf, 1);
pthread_mutex_unlock(&mutex);
} }
- (void) do - (void) do
{ {
pthread_mutex_init(&mutex, NULL); pipe(_fds);
pthread_cond_init(&cond, NULL);
[[[NSThread alloc] initWithTarget:self selector:@selector(runOnce) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(runOnce) object:nil] start];
pthread_mutex_lock(&mutex); char buf[1];
pthread_cond_wait(&cond, &mutex); read(_fds[0], buf, 1);
pthread_mutex_unlock(&mutex); close(_fds[0]);
pthread_cond_destroy(&cond); close(_fds[1]);
pthread_mutex_destroy(&mutex);
} }

2
native/Avalonia.Native/src/OSX/window.h

@ -18,6 +18,8 @@ class WindowBaseImpl;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(void) setCanBecomeKeyAndMain; -(void) setCanBecomeKeyAndMain;
-(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents;
@end @end
struct INSWindowHolder struct INSWindowHolder

125
native/Avalonia.Native/src/OSX/window.mm

@ -109,6 +109,7 @@ public:
if(Window != nullptr) if(Window != nullptr)
{ {
[Window orderOut:Window]; [Window orderOut:Window];
[Window restoreParentWindow];
} }
return S_OK; return S_OK;
@ -392,30 +393,6 @@ protected:
} }
}; };
class ModalDisposable : public ComUnknownObject
{
NSModalSession _session;
AvnWindow* _window;
void Dispose ()
{
[_window orderOut:_window];
[NSApp endModalSession:_session];
}
public:
ModalDisposable(AvnWindow* window, NSModalSession session)
{
_session = session;
_window = window;
}
virtual ~ModalDisposable()
{
Dispose();
}
};
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{ {
private: private:
@ -444,32 +421,27 @@ private:
{ {
@autoreleasepool @autoreleasepool
{ {
if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window];
WindowBaseImpl::Show(); WindowBaseImpl::Show();
return SetWindowState(_lastWindowState); return SetWindowState(_lastWindowState);
} }
} }
virtual HRESULT ShowDialog (IUnknown**ppv) override virtual HRESULT ShowDialog (IAvnWindow* parent) override
{ {
@autoreleasepool @autoreleasepool
{ {
if(ppv == nullptr) if(parent == nullptr)
{
return E_POINTER; return E_POINTER;
}
auto cparent = dynamic_cast<WindowImpl*>(parent);
auto session = [NSApp beginModalSessionForWindow:Window]; if(cparent == nullptr)
auto disposable = new ModalDisposable(Window, session); return E_INVALIDARG;
*ppv = disposable;
SetPosition(lastPositionSet);
UpdateStyle();
[Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window pollModalSession:session]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
return S_OK; return S_OK;
} }
@ -843,8 +815,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super viewDidChangeBackingProperties]; [super viewDidChangeBackingProperties];
} }
- (bool) ignoreUserInput
{
auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
return TRUE;
return FALSE;
}
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{ {
if([self ignoreUserInput])
return;
[self becomeFirstResponder]; [self becomeFirstResponder];
auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [self toAvnPoint:localPoint]; auto avnPoint = [self toAvnPoint:localPoint];
@ -952,7 +935,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
} }
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{ {
if([self ignoreUserInput])
return;
auto key = s_KeyMap[[event keyCode]]; auto key = s_KeyMap[[event keyCode]];
auto timestamp = [event timestamp] * 1000; auto timestamp = [event timestamp] * 1000;
@ -1125,13 +1110,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{ {
ComPtr<WindowBaseImpl> parent = _parent; ComPtr<WindowBaseImpl> parent = _parent;
_parent = NULL; _parent = NULL;
[self restoreParentWindow];
parent->BaseEvents->Closed(); parent->BaseEvents->Closed();
[parent->View onClosed]; [parent->View onClosed];
[self setContentView: nil]; dispatch_async(dispatch_get_main_queue(), ^{
[self setContentView: nil];
});
} }
} }
-(BOOL)canBecomeKeyWindow -(BOOL)canBecomeKeyWindow
{ {
return _canBecomeKeyAndMain; return _canBecomeKeyAndMain;
@ -1142,12 +1129,62 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return _canBecomeKeyAndMain; return _canBecomeKeyAndMain;
} }
-(bool) activateAppropriateChild: (bool)activating
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
[ch activateAppropriateChild:false];
return FALSE;
}
if(!activating)
[self makeKeyAndOrderFront:self];
return TRUE;
}
-(bool)shouldTryToHandleEvents
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
return FALSE;
}
return TRUE;
}
-(void)makeKeyWindow
{
if([self activateAppropriateChild: true])
{
[super makeKeyWindow];
}
}
-(void)becomeKeyWindow -(void)becomeKeyWindow
{ {
_parent->BaseEvents->Activated(); if([self activateAppropriateChild: true])
[super becomeKeyWindow]; {
_parent->BaseEvents->Activated();
[super becomeKeyWindow];
}
}
-(void) restoreParentWindow;
{
auto parent = objc_cast<AvnWindow>([self parentWindow]);
if(parent != nil)
{
[parent removeChildWindow:self];
[parent activateAppropriateChild: false];
}
} }
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
{ {
return true; return true;

2
samples/ControlCatalog.Desktop/Program.cs

@ -22,7 +22,7 @@ namespace ControlCatalog
/// This method is needed for IDE previewer infrastructure /// This method is needed for IDE previewer infrastructure
/// </summary> /// </summary>
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>().LogToDebug().UsePlatformDetect(); => AppBuilder.Configure<App>().LogToDebug().UsePlatformDetect().UseReactiveUI();
private static void ConfigureAssetAssembly(AppBuilder builder) private static void ConfigureAssetAssembly(AppBuilder builder)
{ {

4
samples/ControlCatalog/MainView.xaml

@ -2,9 +2,6 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TabControl Classes="sidebar" Name="Sidebar"> <TabControl Classes="sidebar" Name="Sidebar">
<TabControl.PageTransition>
<CrossFade Duration="0.25"/>
</TabControl.PageTransition>
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem> <TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem> <TabItem Header="Border"><pages:BorderPage/></TabItem>
<TabItem Header="Button"><pages:ButtonPage/></TabItem> <TabItem Header="Button"><pages:ButtonPage/></TabItem>
@ -30,5 +27,6 @@
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem> <TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem> <TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem> <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
</TabControl> </TabControl>
</UserControl> </UserControl>

5
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -3,10 +3,7 @@
<Button Name="OpenFile">Open File</Button> <Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button> <Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button> <Button Name="SelectFolder">Select Folder</Button>
<StackPanel Orientation="Horizontal">
<CheckBox Name="IsModal" IsChecked="True"/>
<TextBlock>Modal to window</TextBlock>
</StackPanel>
<Button Name="DecoratedWindow">Decorated window</Button> <Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="Dialog">Dialog</Button>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

14
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -31,12 +31,18 @@ namespace ControlCatalog.Pages
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
}; };
this.FindControl<Button>("DecoratedWindow").Click += delegate this.FindControl<Button>("DecoratedWindow").Click += delegate
{ {
new DecoratedWindow().Show(); new DecoratedWindow().ShowDialog(GetWindow());
}; };
this.FindControl<Button>("Dialog").Click += delegate
{
new MainWindow().ShowDialog(GetWindow());
};
} }
Window GetWindow() => this.FindControl<CheckBox>("IsModal").IsChecked.Value ? (Window)this.VisualRoot : null; Window GetWindow() => (Window)this.VisualRoot;
private void InitializeComponent() private void InitializeComponent()
{ {

2
samples/ControlCatalog/Pages/MenuPage.xaml.cs

@ -83,7 +83,7 @@ namespace ControlCatalog.Pages
public async Task Open() public async Task Open()
{ {
var dialog = new OpenFileDialog(); var dialog = new OpenFileDialog();
var result = await dialog.ShowAsync(); var result = await dialog.ShowAsync(App.Current.MainWindow);
if (result != null) if (result != null)
{ {

16
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -6,7 +6,7 @@
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock> <TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto"> <Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35"> <Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/> <CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
@ -20,7 +20,7 @@
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/> <CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
</Grid> </Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35"> <Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}" <DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2"> VerticalAlignment="Center" Margin="2">
@ -49,22 +49,22 @@
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock> <TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" /> <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid> </Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120"> <Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, 120">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}" <NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}" <NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center" <NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center" <NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> Margin="2" Width="70" HorizontalAlignment="Center"/>
</Grid> </Grid>
</Grid> </Grid>
@ -72,7 +72,7 @@
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10"> <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock> <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100" CultureInfo="en-US" VerticalAlignment="Center" Width="100"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/> Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel> </StackPanel>

124
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -0,0 +1,124 @@
<UserControl xmlns="https://github.com/avaloniaui">
<DockPanel>
<TextBlock
DockPanel.Dock="Top"
Classes="h1"
Text="TabControl"
Margin="4">
</TextBlock>
<TextBlock
DockPanel.Dock="Top"
Classes="h2"
Text="A tab control that displays a tab strip along with the content of the selected tab"
Margin="4">
</TextBlock>
<Grid
ColumnDefinitions="*,*"
RowDefinitions="*,100">
<DockPanel
Grid.Column="0"
Margin="4">
<TextBlock
DockPanel.Dock="Top"
Classes="h1"
Text="From Inline TabItems">
</TextBlock>
<TabControl
Margin="0 16"
TabStripPlacement="{Binding TabPlacement}">
<TabItem>
<TabItem.Header>
<TextBlock
Text="Arch"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="8">
</TextBlock>
</TabItem.Header>
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock>This is the first page in the TabControl.</TextBlock>
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg" Width="300"/>
</StackPanel>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock
Text="Leaf"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="8">
</TextBlock>
</TabItem.Header>
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock>This is the second page in the TabControl.</TextBlock>
<Image Source="resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg" Width="300"/>
</StackPanel>
</TabItem>
<TabItem IsEnabled="False">
<TabItem.Header>
<TextBlock
Text="Disabled"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="8">
</TextBlock>
</TabItem.Header>
<TextBlock>You should not see this.</TextBlock>
</TabItem>
</TabControl>
</DockPanel>
<DockPanel
Grid.Column="1"
Margin="4">
<TextBlock
DockPanel.Dock="Top"
Classes="h1"
Text="From DataTemplate">
</TextBlock>
<TabControl
Items="{Binding Tabs}"
Margin="0 16"
TabStripPlacement="{Binding TabPlacement}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Header}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="8">
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="{Binding Text}"/>
<Image Source="{Binding Image}" Width="300"/>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.Styles>
<Style Selector="TabItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabControl.Styles>
</TabControl>
</DockPanel>
<StackPanel
Grid.Row="1"
Grid.ColumnSpan="2"
Orientation="Horizontal"
Spacing="8"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
<DropDown SelectedIndex="{Binding TabPlacement, Mode=TwoWay}">
<DropDownItem>Left</DropDownItem>
<DropDownItem>Bottom</DropDownItem>
<DropDownItem>Right</DropDownItem>
<DropDownItem>Top</DropDownItem>
</DropDown>
</StackPanel>
</Grid>
</DockPanel>
</UserControl>

80
samples/ControlCatalog/Pages/TabControlPage.xaml.cs

@ -0,0 +1,80 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using ReactiveUI;
namespace ControlCatalog.Pages
{
using System.Collections.Generic;
public class TabControlPage : UserControl
{
public TabControlPage()
{
InitializeComponent();
DataContext = new PageViewModel
{
Tabs = new[]
{
new TabItemViewModel
{
Header = "Arch",
Text = "This is the first templated tab page.",
Image = LoadBitmap("resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg?assembly=ControlCatalog"),
},
new TabItemViewModel
{
Header = "Leaf",
Text = "This is the second templated tab page.",
Image = LoadBitmap("resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg?assembly=ControlCatalog"),
},
new TabItemViewModel
{
Header = "Disabled",
Text = "You should not see this.",
IsEnabled = false,
},
},
TabPlacement = Dock.Top,
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private IBitmap LoadBitmap(string uri)
{
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
return new Bitmap(assets.Open(new Uri(uri)));
}
private class PageViewModel : ReactiveObject
{
private Dock _tabPlacement;
public TabItemViewModel[] Tabs { get; set; }
public Dock TabPlacement
{
get { return _tabPlacement; }
set { this.RaiseAndSetIfChanged(ref _tabPlacement, value); }
}
}
private class TabItemViewModel
{
public string Header { get; set; }
public string Text { get; set; }
public IBitmap Image { get; set; }
public bool IsEnabled { get; set; } = true;
}
}
}

111
samples/ControlCatalog/SideBar.xaml

@ -1,52 +1,67 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<Style Selector="TabControl.sidebar"> <Style Selector="TabControl.sidebar">
<Setter Property="Template"> <Setter Property="TabStripPlacement" Value="Left"/>
<ControlTemplate> <Setter Property="Padding" Value="8 0 0 0"/>
<DockPanel> <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}"/>
<ScrollViewer MinWidth="190" Background="{DynamicResource ThemeAccentBrush}" DockPanel.Dock="Left"> <Setter Property="Template">
<TabStrip Name="PART_TabStrip" <ControlTemplate>
MemberSelector="{x:Static TabControl.HeaderSelector}" <Border
Items="{TemplateBinding Items}" Margin="{TemplateBinding Margin}"
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"> BorderBrush="{TemplateBinding BorderBrush}"
<TabStrip.ItemsPanel> BorderThickness="{TemplateBinding BorderThickness}">
<ItemsPanelTemplate> <DockPanel>
<StackPanel Orientation="Vertical"/> <ScrollViewer
</ItemsPanelTemplate> Name="PART_ScrollViewer"
</TabStrip.ItemsPanel> HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
</TabStrip> VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
</ScrollViewer> Background="{TemplateBinding Background}">
<Carousel Name="PART_Content" <ItemsPresenter
Margin="8 0 0 0" Name="PART_ItemsPresenter"
MemberSelector="{x:Static TabControl.ContentSelector}" MinWidth="190"
Items="{TemplateBinding Items}" Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding SelectedIndex}" ItemsPanel="{TemplateBinding ItemsPanel}"
PageTransition="{TemplateBinding PageTransition}" ItemTemplate="{TemplateBinding ItemTemplate}"
Grid.Row="1"/> MemberSelector="{TemplateBinding MemberSelector}">
</DockPanel> </ItemsPresenter>
</ControlTemplate> </ScrollViewer>
</Setter> <ContentPresenter
</Style> Name="PART_Content"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabControl.sidebar TabStripItem"> <Style Selector="TabControl.sidebar > TabItem">
<Setter Property="Foreground" Value="White"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="14"/> <Setter Property="Foreground" Value="White"/>
<Setter Property="Margin" Value="0"/> <Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="16"/> <Setter Property="Margin" Value="0"/>
<Setter Property="Opacity" Value="0.5"/> <Setter Property="Padding" Value="16"/>
<Setter Property="Transitions"> <Setter Property="Opacity" Value="0.5"/>
<Transitions> <Setter Property="Transitions">
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/> <Transitions>
</Transitions> <DoubleTransition Property="Opacity" Duration="0:0:0.5"/>
</Setter> </Transitions>
</Style> </Setter>
</Style>
<Style Selector="TabControl.sidebar TabStripItem:pointerover"> <Style Selector="TabControl.sidebar > TabItem:pointerover">
<Setter Property="Opacity" Value="1"/> <Setter Property="Opacity" Value="1"/>
</Style> </Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Style Selector="TabControl.sidebar TabStripItem:selected"> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/> </Style>
<Setter Property="Opacity" Value="1"/> <Style Selector="TabControl.sidebar > TabItem:selected">
</Style> <Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
</Style>
</Styles> </Styles>

1
scripts/ReplaceNugetCache.sh

@ -4,5 +4,6 @@
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

12
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -88,18 +88,21 @@ namespace Avalonia.Data.Core
_subscriber(value); _subscriber(value);
} }
protected void ValueChanged(object value) protected void ValueChanged(object value) => ValueChanged(value, true);
private void ValueChanged(object value, bool notify)
{ {
var notification = value as BindingNotification; var notification = value as BindingNotification;
if (notification == null) if (notification == null)
{ {
LastValue = new WeakReference(value); LastValue = new WeakReference(value);
if (Next != null) if (Next != null)
{ {
Next.Target = new WeakReference(value); Next.Target = LastValue;
} }
else else if (notify)
{ {
_subscriber(value); _subscriber(value);
} }
@ -110,7 +113,7 @@ namespace Avalonia.Data.Core
if (Next != null) if (Next != null)
{ {
Next.Target = new WeakReference(notification.Value); Next.Target = LastValue;
} }
if (Next == null || notification.Error != null) if (Next == null || notification.Error != null)
@ -136,6 +139,7 @@ namespace Avalonia.Data.Core
} }
else else
{ {
ValueChanged(AvaloniaProperty.UnsetValue, notify:false);
_listening = false; _listening = false;
} }
} }

2
src/Avalonia.Controls/ColumnDefinition.cs

@ -88,4 +88,4 @@ namespace Avalonia.Controls
set { SetValue(WidthProperty, value); } set { SetValue(WidthProperty, value); }
} }
} }
} }

57
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -0,0 +1,57 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Generators
{
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem>
{
public TabItemContainerGenerator(TabControl owner)
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty)
{
Owner = owner;
}
public new TabControl Owner { get; }
protected override IControl CreateContainer(object item)
{
var tabItem = (TabItem)base.CreateContainer(item);
tabItem.ParentTabControl = Owner;
if (tabItem.HeaderTemplate == null)
{
tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty];
}
if (tabItem.Header == null)
{
if (item is IHeadered headered)
{
tabItem.Header = headered.Header;
}
else
{
if (!(tabItem.DataContext is IControl))
{
tabItem.Header = tabItem.DataContext;
}
}
}
if (!(tabItem.Content is IControl))
{
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
}
if (tabItem.Content == null)
{
tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
}
return tabItem;
}
}
}

154
src/Avalonia.Controls/Grid.cs

@ -3,10 +3,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.VisualTree;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -44,6 +47,24 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> RowSpanProperty = public static readonly AttachedProperty<int> RowSpanProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>("RowSpan", 1); AvaloniaProperty.RegisterAttached<Grid, Control, int>("RowSpan", 1);
public static readonly AttachedProperty<bool> IsSharedSizeScopeProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, bool>("IsSharedSizeScope", false);
protected override void OnMeasureInvalidated()
{
base.OnMeasureInvalidated();
_sharedSizeHost?.InvalidateMeasure(this);
}
private SharedSizeScopeHost _sharedSizeHost;
/// <summary>
/// Defines the SharedSizeScopeHost private property.
/// The ampersands are used to make accessing the property via xaml inconvenient.
/// </summary>
internal static readonly AttachedProperty<SharedSizeScopeHost> s_sharedSizeScopeHostProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, SharedSizeScopeHost>("&&SharedSizeScopeHost");
private ColumnDefinitions _columnDefinitions; private ColumnDefinitions _columnDefinitions;
private RowDefinitions _rowDefinitions; private RowDefinitions _rowDefinitions;
@ -51,6 +72,13 @@ namespace Avalonia.Controls
static Grid() static Grid()
{ {
AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty);
IsSharedSizeScopeProperty.Changed.AddClassHandler<Control>(IsSharedSizeScopeChanged);
}
public Grid()
{
this.AttachedToVisualTree += Grid_AttachedToVisualTree;
this.DetachedFromVisualTree += Grid_DetachedFromVisualTree;
} }
/// <summary> /// <summary>
@ -77,6 +105,7 @@ namespace Avalonia.Controls
_columnDefinitions = value; _columnDefinitions = value;
_columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure());
_columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure();
} }
} }
@ -104,6 +133,7 @@ namespace Avalonia.Controls
_rowDefinitions = value; _rowDefinitions = value;
_rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure());
_rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure();
} }
} }
@ -271,6 +301,11 @@ namespace Avalonia.Controls
_rowLayoutCache = rowLayout; _rowLayoutCache = rowLayout;
_columnLayoutCache = columnLayout; _columnLayoutCache = columnLayout;
if (_sharedSizeHost?.ParticipatesInScope(this) ?? false)
{
_sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult);
}
return new Size(columnResult.DesiredLength, rowResult.DesiredLength); return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once. // Measure each child only once.
@ -319,9 +354,21 @@ namespace Avalonia.Controls
var (safeColumns, safeRows) = GetSafeColumnRows(); var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = _columnLayoutCache; var columnLayout = _columnLayoutCache;
var rowLayout = _rowLayoutCache; var rowLayout = _rowLayoutCache;
var rowCache = _rowMeasureCache;
var columnCache = _columnMeasureCache;
if (_sharedSizeHost?.ParticipatesInScope(this) ?? false)
{
(rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache);
rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList);
columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList);
}
// Calculate for arrange result. // Calculate for arrange result.
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var columnResult = columnLayout.Arrange(finalSize.Width, columnCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, rowCache);
// Arrange the children. // Arrange the children.
foreach (var child in Children.OfType<Control>()) foreach (var child in Children.OfType<Control>())
{ {
@ -350,6 +397,73 @@ namespace Avalonia.Controls
return finalSize; return finalSize;
} }
/// <summary>
/// Tests whether this grid belongs to a shared size scope.
/// </summary>
/// <returns>True if the grid is registered in a shared size scope.</returns>
internal bool HasSharedSizeScope()
{
return _sharedSizeHost != null;
}
/// <summary>
/// Called when the SharedSizeScope for a given grid has changed.
/// Unregisters the grid from it's current scope and finds a new one (if any)
/// </summary>
/// <remarks>
/// This method, while not efficient, correctly handles nested scopes, with any order of scope changes.
/// </remarks>
internal void SharedScopeChanged()
{
_sharedSizeHost?.UnegisterGrid(this);
_sharedSizeHost = null;
var scope = this.GetVisualAncestors().OfType<Control>()
.FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty));
if (scope != null)
{
_sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty);
_sharedSizeHost.RegisterGrid(this);
}
InvalidateMeasure();
}
/// <summary>
/// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid
/// in it.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
var scope =
new Control[] { this }.Concat(this.GetVisualAncestors().OfType<Control>())
.FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty));
if (_sharedSizeHost != null)
throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!");
if (scope != null)
{
_sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty);
_sharedSizeHost.RegisterGrid(this);
}
}
/// <summary>
/// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_sharedSizeHost?.UnegisterGrid(this);
_sharedSizeHost = null;
}
/// <summary> /// <summary>
/// Get the safe column/columnspan and safe row/rowspan. /// Get the safe column/columnspan and safe row/rowspan.
/// This method ensures that none of the children has a column/row outside the bounds of the definitions. /// This method ensures that none of the children has a column/row outside the bounds of the definitions.
@ -426,5 +540,41 @@ namespace Avalonia.Controls
return value; return value;
} }
/// <summary>
/// Called when the value of <see cref="Grid.IsSharedSizeScopeProperty"/> changes for a control.
/// </summary>
/// <param name="source">The control that triggered the change.</param>
/// <param name="arg2">Change arguments.</param>
private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2)
{
var shouldDispose = (arg2.OldValue is bool d) && d;
if (shouldDispose)
{
var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost;
if (host == null)
throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!");
host.Dispose();
source.ClearValue(s_sharedSizeScopeHostProperty);
}
var shouldAssign = (arg2.NewValue is bool a) && a;
if (shouldAssign)
{
if (source.GetValue(s_sharedSizeScopeHostProperty) != null)
throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!");
source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost());
}
// if the scope has changed, notify the descendant grids that they need to update.
if (source.GetVisualRoot() != null && shouldAssign || shouldDispose)
{
var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType<Grid>();
foreach (var grid in participatingGrids)
grid.SharedScopeChanged();
}
}
} }
} }

73
src/Avalonia.Controls/GridSplitter.cs

@ -44,26 +44,52 @@ namespace Avalonia.Controls
protected override void OnDragDelta(VectorEventArgs e) protected override void OnDragDelta(VectorEventArgs e)
{ {
// WPF doesn't change anything when spliter is in the last row/column
// but resizes the splitter row/column when it's the first one.
// this is different, but more internally consistent.
if (_prevDefinition == null || _nextDefinition == null)
return;
var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y;
double max; double max;
double min; double min;
GetDeltaConstraints(out min, out max); GetDeltaConstraints(out min, out max);
delta = Math.Min(Math.Max(delta, min), max); delta = Math.Min(Math.Max(delta, min), max);
foreach (var definition in _definitions)
var prevIsStar = IsStar(_prevDefinition);
var nextIsStar = IsStar(_nextDefinition);
if (prevIsStar && nextIsStar)
{ {
if (definition == _prevDefinition) foreach (var definition in _definitions)
{
SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else if (definition == _nextDefinition)
{
SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (IsStar(definition))
{ {
SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. if (definition == _prevDefinition)
{
SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else if (definition == _nextDefinition)
{
SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (IsStar(definition))
{
SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
}
} }
} }
else if (prevIsStar)
{
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
else if (nextIsStar)
{
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
}
else
{
SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
}
} }
private double GetActualLength(DefinitionBase definition) private double GetActualLength(DefinitionBase definition)
@ -71,7 +97,7 @@ namespace Avalonia.Controls
if (definition == null) if (definition == null)
return 0; return 0;
var columnDefinition = definition as ColumnDefinition; var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.ActualWidth ?? ((RowDefinition) definition).ActualHeight; return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
} }
private double GetMinLength(DefinitionBase definition) private double GetMinLength(DefinitionBase definition)
@ -79,7 +105,7 @@ namespace Avalonia.Controls
if (definition == null) if (definition == null)
return 0; return 0;
var columnDefinition = definition as ColumnDefinition; var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MinWidth ?? ((RowDefinition) definition).MinHeight; return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
} }
private double GetMaxLength(DefinitionBase definition) private double GetMaxLength(DefinitionBase definition)
@ -87,13 +113,13 @@ namespace Avalonia.Controls
if (definition == null) if (definition == null)
return 0; return 0;
var columnDefinition = definition as ColumnDefinition; var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.MaxWidth ?? ((RowDefinition) definition).MaxHeight; return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
} }
private bool IsStar(DefinitionBase definition) private bool IsStar(DefinitionBase definition)
{ {
var columnDefinition = definition as ColumnDefinition; var columnDefinition = definition as ColumnDefinition;
return columnDefinition?.Width.IsStar ?? ((RowDefinition) definition).Height.IsStar; return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
} }
private void SetLengthInStars(DefinitionBase definition, double value) private void SetLengthInStars(DefinitionBase definition, double value)
@ -105,7 +131,20 @@ namespace Avalonia.Controls
} }
else else
{ {
((RowDefinition) definition).Height = new GridLength(value, GridUnitType.Star); ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star);
}
}
private void SetLength(DefinitionBase definition, double value)
{
var columnDefinition = definition as ColumnDefinition;
if (columnDefinition != null)
{
columnDefinition.Width = new GridLength(value);
}
else
{
((RowDefinition)definition).Height = new GridLength(value);
} }
} }
@ -160,7 +199,7 @@ namespace Avalonia.Controls
} }
if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column
.Where(c => Grid.GetColumn(c) == col) .Where(c => Grid.GetColumn(c) == col)
.Any(c => c.GetType() != typeof (GridSplitter))) .Any(c => c.GetType() != typeof(GridSplitter)))
{ {
return Orientation.Horizontal; return Orientation.Horizontal;
} }

2
src/Avalonia.Controls/ItemsControl.cs

@ -249,8 +249,6 @@ namespace Avalonia.Controls
if (containerControl != null) if (containerControl != null)
{ {
((ISetLogicalParent)containerControl).SetParent(this); ((ISetLogicalParent)containerControl).SetParent(this);
containerControl.SetValue(TemplatedParentProperty, null);
containerControl.UpdateChild(); containerControl.UpdateChild();
if (containerControl.Child != null) if (containerControl.Child != null)

5
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -30,10 +30,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Shows the window as a dialog. /// Shows the window as a dialog.
/// </summary> /// </summary>
/// <returns> void ShowDialog(IWindowImpl parent);
/// An <see cref="IDisposable"/> that should be used to close the window.
/// </returns>
IDisposable ShowDialog();
/// <summary> /// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc) /// Enables or disables system window decorations (title bar, buttons, etc)

21
src/Avalonia.Controls/Primitives/HeaderedContentControl.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Templates;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
/// <summary> /// <summary>
@ -12,7 +14,13 @@ namespace Avalonia.Controls.Primitives
/// Defines the <see cref="Header"/> property. /// Defines the <see cref="Header"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<object> HeaderProperty = public static readonly StyledProperty<object> HeaderProperty =
AvaloniaProperty.Register<ContentControl, object>(nameof(Header)); AvaloniaProperty.Register<HeaderedContentControl, object>(nameof(Header));
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> HeaderTemplateProperty =
AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate>(nameof(HeaderTemplate));
/// <summary> /// <summary>
/// Gets or sets the header content. /// Gets or sets the header content.
@ -21,6 +29,15 @@ namespace Avalonia.Controls.Primitives
{ {
get { return GetValue(HeaderProperty); } get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); } set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
public IDataTemplate HeaderTemplate
{
get { return GetValue(HeaderTemplateProperty); }
set { SetValue(HeaderTemplateProperty, value); }
} }
} }
} }

6
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -84,8 +84,10 @@ namespace Avalonia.Controls.Primitives
if (screen != null) if (screen != null)
{ {
var screenX = Position.X + Bounds.Width - screen.Bounds.X; var scaling = VisualRoot.RenderScaling;
var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
var screenX = Position.X + (Bounds.Width * scaling) - screen.Bounds.X;
var screenY = Position.Y + (Bounds.Height * scaling) - screen.Bounds.Y;
if (screenX > screen.Bounds.Width) if (screenX > screen.Bounds.Width)
{ {

19
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -260,7 +260,7 @@ namespace Avalonia.Controls.Primitives
var child = template.Build(this); var child = template.Build(this);
var nameScope = new NameScope(); var nameScope = new NameScope();
NameScope.SetNameScope((Control)child, nameScope); NameScope.SetNameScope((Control)child, nameScope);
child.SetValue(TemplatedParentProperty, this); ApplyTemplatedParent(child);
RegisterNames(child, nameScope); RegisterNames(child, nameScope);
((ISetLogicalParent)child).SetParent(this); ((ISetLogicalParent)child).SetParent(this);
VisualChildren.Add(child); VisualChildren.Add(child);
@ -326,6 +326,23 @@ namespace Avalonia.Controls.Primitives
InvalidateMeasure(); InvalidateMeasure();
} }
/// <summary>
/// Sets the TemplatedParent property for the created template children.
/// </summary>
/// <param name="control">The control.</param>
private void ApplyTemplatedParent(IControl control)
{
control.SetValue(TemplatedParentProperty, this);
foreach (var child in control.LogicalChildren)
{
if (child is IControl c)
{
ApplyTemplatedParent(c);
}
}
}
/// <summary> /// <summary>
/// Registers each control with its name scope. /// Registers each control with its name scope.
/// </summary> /// </summary>

31
src/Avalonia.Controls/SystemDialog.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -18,28 +19,40 @@ namespace Avalonia.Controls
public class SaveFileDialog : FileDialog public class SaveFileDialog : FileDialog
{ {
public string DefaultExtension { get; set; } public string DefaultExtension { get; set; }
public async Task<string> ShowAsync(Window window) public async Task<string> ShowAsync(Window parent)
=> {
((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, window?.PlatformImpl)) ?? if(parent == null)
new string[0]).FirstOrDefault(); throw new ArgumentNullException(nameof(parent));
return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
.ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
new string[0]).FirstOrDefault();
}
} }
public class OpenFileDialog : FileDialog public class OpenFileDialog : FileDialog
{ {
public bool AllowMultiple { get; set; } public bool AllowMultiple { get; set; }
public Task<string[]> ShowAsync(Window window = null) public Task<string[]> ShowAsync(Window parent)
=> AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, window?.PlatformImpl); {
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent?.PlatformImpl);
}
} }
public class OpenFolderDialog : FileSystemDialog public class OpenFolderDialog : FileSystemDialog
{ {
public string DefaultDirectory { get; set; } public string DefaultDirectory { get; set; }
public Task<string> ShowAsync(Window window = null) public Task<string> ShowAsync(Window parent)
=> AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, window?.PlatformImpl); {
if(parent == null)
throw new ArgumentNullException(nameof(parent));
return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent?.PlatformImpl);
}
} }
public abstract class SystemDialog public abstract class SystemDialog

164
src/Avalonia.Controls/TabControl.cs

@ -1,10 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -14,28 +16,46 @@ namespace Avalonia.Controls
public class TabControl : SelectingItemsControl public class TabControl : SelectingItemsControl
{ {
/// <summary> /// <summary>
/// Defines the <see cref="PageTransition"/> property. /// Defines the <see cref="TabStripPlacement"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IPageTransition> PageTransitionProperty = public static readonly StyledProperty<Dock> TabStripPlacementProperty =
Avalonia.Controls.Carousel.PageTransitionProperty.AddOwner<TabControl>(); AvaloniaProperty.Register<TabControl, Dock>(nameof(TabStripPlacement), defaultValue: Dock.Top);
/// <summary> /// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>. /// Defines the <see cref="HorizontalContentAlignment"/> property.
/// </summary> /// </summary>
public static readonly IMemberSelector ContentSelector = public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
new FuncMemberSelector<object, object>(SelectContent); ContentControl.HorizontalContentAlignmentProperty.AddOwner<TabControl>();
/// <summary> /// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the header of a <see cref="TabItem"/>. /// Defines the <see cref="VerticalContentAlignment"/> property.
/// </summary> /// </summary>
public static readonly IMemberSelector HeaderSelector = public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
new FuncMemberSelector<object, object>(SelectHeader); ContentControl.VerticalContentAlignmentProperty.AddOwner<TabControl>();
/// <summary> /// <summary>
/// Defines the <see cref="TabStripPlacement"/> property. /// Defines the <see cref="ContentTemplate"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty = public static readonly StyledProperty<IDataTemplate> ContentTemplateProperty =
AvaloniaProperty.Register<TabControl, Dock>(nameof(TabStripPlacement), defaultValue: Dock.Top); ContentControl.ContentTemplateProperty.AddOwner<TabControl>();
/// <summary>
/// The selected content property
/// </summary>
public static readonly StyledProperty<object> SelectedContentProperty =
AvaloniaProperty.Register<TabControl, object>(nameof(SelectedContent));
/// <summary>
/// The selected content template property
/// </summary>
public static readonly StyledProperty<IDataTemplate> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate>(nameof(SelectedContentTemplate));
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new WrapPanel());
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TabControl"/> class. /// Initializes static members of the <see cref="TabControl"/> class.
@ -43,107 +63,107 @@ namespace Avalonia.Controls
static TabControl() static TabControl()
{ {
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false); ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty); AffectsMeasure<TabControl>(TabStripPlacementProperty);
} }
/// <summary> /// <summary>
/// Gets the pages portion of the <see cref="TabControl"/>'s template. /// Gets or sets the horizontal alignment of the content within the control.
/// </summary> /// </summary>
public IControl Pages public HorizontalAlignment HorizontalContentAlignment
{ {
get; get { return GetValue(HorizontalContentAlignmentProperty); }
private set; set { SetValue(HorizontalContentAlignmentProperty, value); }
} }
/// <summary> /// <summary>
/// Gets the tab strip portion of the <see cref="TabControl"/>'s template. /// Gets or sets the vertical alignment of the content within the control.
/// </summary> /// </summary>
public IControl TabStrip public VerticalAlignment VerticalContentAlignment
{ {
get; get { return GetValue(VerticalContentAlignmentProperty); }
private set; set { SetValue(VerticalContentAlignmentProperty, value); }
} }
/// <summary> /// <summary>
/// Gets or sets the transition to use when switching tabs. /// Gets or sets the tabstrip placement of the TabControl.
/// </summary> /// </summary>
public IPageTransition PageTransition public Dock TabStripPlacement
{ {
get { return GetValue(PageTransitionProperty); } get { return GetValue(TabStripPlacementProperty); }
set { SetValue(PageTransitionProperty, value); } set { SetValue(TabStripPlacementProperty, value); }
} }
/// <summary> /// <summary>
/// Gets or sets the tabstrip placement of the tabcontrol. /// Gets or sets the default data template used to display the content of the selected tab.
/// </summary> /// </summary>
public Dock TabStripPlacement public IDataTemplate ContentTemplate
{ {
get { return GetValue(TabStripPlacementProperty); } get { return GetValue(ContentTemplateProperty); }
set { SetValue(TabStripPlacementProperty, value); } set { SetValue(ContentTemplateProperty, value); }
} }
/// <summary>
/// Gets or sets the content of the selected tab.
/// </summary>
/// <value>
/// The content of the selected tab.
/// </value>
public object SelectedContent
{
get { return GetValue(SelectedContentProperty); }
internal set { SetValue(SelectedContentProperty, value); }
}
/// <summary>
/// Gets or sets the content template for the selected tab.
/// </summary>
/// <value>
/// The content template of the selected tab.
/// </value>
public IDataTemplate SelectedContentTemplate
{
get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); }
}
internal ItemsPresenter ItemsPresenterPart { get; private set; }
internal ContentPresenter ContentPart { get; private set; }
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
// TabControl doesn't actually create items - instead its TabStrip and Carousel return new TabItemContainerGenerator(this);
// children create the items. However we want it to be a SelectingItemsControl
// so that it has the Items/SelectedItem etc properties. In this case, we can
// return a null ItemContainerGenerator to disable the creation of item containers.
return null;
} }
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
base.OnTemplateApplied(e); base.OnTemplateApplied(e);
TabStrip = e.NameScope.Find<IControl>("PART_TabStrip"); ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
Pages = e.NameScope.Find<IControl>("PART_Content");
ContentPart = e.NameScope.Get<ContentPresenter>("PART_Content");
} }
/// <summary> /// <inheritdoc/>
/// Selects the content of a tab item. protected override void OnGotFocus(GotFocusEventArgs e)
/// </summary>
/// <param name="o">The tab item.</param>
/// <returns>The content.</returns>
private static object SelectContent(object o)
{ {
var content = o as IContentControl; base.OnGotFocus(e);
if (content != null) if (e.NavigationMethod == NavigationMethod.Directional)
{ {
return content.Content; e.Handled = UpdateSelectionFromEventSource(e.Source);
} }
else
{
return o;
}
} }
/// <summary> /// <inheritdoc/>
/// Selects the header of a tab item. protected override void OnPointerPressed(PointerPressedEventArgs e)
/// </summary>
/// <param name="o">The tab item.</param>
/// <returns>The content.</returns>
private static object SelectHeader(object o)
{ {
var headered = o as IHeadered; base.OnPointerPressed(e);
var control = o as IControl;
if (headered != null) if (e.MouseButton == MouseButton.Left)
{
return headered.Header ?? string.Empty;
}
else if (control != null)
{
// Non-headered control items should result in TabStripItems with empty content.
// If a TabStrip is created with non IHeadered controls as its items, don't try to
// display the control in the TabStripItem: the content portion will also try to
// display this control, resulting in dual-parentage breakage.
return string.Empty;
}
else
{ {
return o; e.Handled = UpdateSelectionFromEventSource(e.Source);
} }
} }
} }

73
src/Avalonia.Controls/TabItem.cs

@ -11,12 +11,20 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class TabItem : HeaderedContentControl, ISelectable public class TabItem : HeaderedContentControl, ISelectable
{ {
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty =
TabControl.TabStripPlacementProperty.AddOwner<TabItem>();
/// <summary> /// <summary>
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<TabItem>(); ListBoxItem.IsSelectedProperty.AddOwner<TabItem>();
private TabControl _parentTabControl;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TabItem"/> class. /// Initializes static members of the <see cref="TabItem"/> class.
/// </summary> /// </summary>
@ -24,6 +32,19 @@ namespace Avalonia.Controls
{ {
SelectableMixin.Attach<TabItem>(IsSelectedProperty); SelectableMixin.Attach<TabItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
IsSelectedProperty.Changed.AddClassHandler<TabItem>(x => x.UpdateSelectedContent);
DataContextProperty.Changed.AddClassHandler<TabItem>(x => x.UpdateHeader);
}
/// <summary>
/// Gets the tab strip placement.
/// </summary>
/// <value>
/// The tab strip placement.
/// </value>
public Dock TabStripPlacement
{
get { return GetValue(TabStripPlacementProperty); }
} }
/// <summary> /// <summary>
@ -34,5 +55,57 @@ namespace Avalonia.Controls
get { return GetValue(IsSelectedProperty); } get { return GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); } set { SetValue(IsSelectedProperty, value); }
} }
internal TabControl ParentTabControl
{
get => _parentTabControl;
set => _parentTabControl = value;
}
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
if (Header == null)
{
if (obj.NewValue is IHeadered headered)
{
if (Header != headered.Header)
{
Header = headered.Header;
}
}
else
{
if (!(obj.NewValue is IControl))
{
Header = obj.NewValue;
}
}
}
else
{
if (Header == obj.OldValue)
{
Header = obj.NewValue;
}
}
}
private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
{
if (!IsSelected)
{
return;
}
if (ParentTabControl.SelectedContentTemplate != ContentTemplate)
{
ParentTabControl.SelectedContentTemplate = ContentTemplate;
}
if (ParentTabControl.SelectedContent != Content)
{
ParentTabControl.SelectedContent = Content;
}
}
} }
} }

6
src/Avalonia.Controls/Templates/TemplateExtensions.cs

@ -24,12 +24,14 @@ namespace Avalonia.Controls.Templates
{ {
foreach (IControl child in control.GetVisualChildren()) foreach (IControl child in control.GetVisualChildren())
{ {
if (child.TemplatedParent == templatedParent) var childTemplatedParent = child.TemplatedParent;
if (childTemplatedParent == templatedParent)
{ {
yield return child; yield return child;
} }
if (child.TemplatedParent != null) if (childTemplatedParent != null)
{ {
foreach (var descendant in GetTemplateChildren(child, templatedParent)) foreach (var descendant in GetTemplateChildren(child, templatedParent))
{ {

1
src/Avalonia.Controls/TextBox.cs

@ -262,7 +262,6 @@ namespace Avalonia.Controls
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_presenter.Cursor = new Cursor(StandardCursorType.Ibeam);
if (IsFocused) if (IsFocused)
{ {

23
src/Avalonia.Controls/Utils/GridLayout.cs

@ -143,14 +143,17 @@ namespace Avalonia.Controls.Utils
/// <param name="containerLength"> /// <param name="containerLength">
/// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method. /// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method.
/// </param> /// </param>
/// <param name="conventions">
/// Overriding conventions that allows the algorithm to handle external inputa
/// </param>
/// <returns> /// <returns>
/// The measured result that containing the desired size and all the column/row lengths. /// The measured result that containing the desired size and all the column/row lengths.
/// </returns> /// </returns>
[NotNull, Pure] [NotNull, Pure]
internal MeasureResult Measure(double containerLength) internal MeasureResult Measure(double containerLength, IReadOnlyList<LengthConvention> conventions = null)
{ {
// Prepare all the variables that this method needs to use. // Prepare all the variables that this method needs to use.
var conventions = _conventions.Select(x => x.Clone()).ToList(); conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList();
var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
var aggregatedLength = 0.0; var aggregatedLength = 0.0;
double starUnitLength; double starUnitLength;
@ -248,7 +251,7 @@ namespace Avalonia.Controls.Utils
// | min | max | | | min | | min max | max | // | min | max | | | min | | min max | max |
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
var desiredStarMin = AggregateAdditionalConventionsForStars(conventions); var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions);
aggregatedLength += desiredStarMin; aggregatedLength += desiredStarMin;
// M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength.
@ -282,7 +285,7 @@ namespace Avalonia.Controls.Utils
// Returns the measuring result. // Returns the measuring result.
return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, return new MeasureResult(containerLength, desiredLength, greedyDesiredLength,
conventions, dynamicConvention); conventions, dynamicConvention, minLengths);
} }
/// <summary> /// <summary>
@ -306,14 +309,14 @@ namespace Avalonia.Controls.Utils
if (finalLength - measure.ContainerLength > LayoutTolerance) if (finalLength - measure.ContainerLength > LayoutTolerance)
{ {
// If the final length is larger, we will rerun the whole measure. // If the final length is larger, we will rerun the whole measure.
measure = Measure(finalLength); measure = Measure(finalLength, measure.LeanLengthList);
} }
else if (finalLength - measure.ContainerLength < -LayoutTolerance) else if (finalLength - measure.ContainerLength < -LayoutTolerance)
{ {
// If the final length is smaller, we measure the M6/6 procedure only. // If the final length is smaller, we measure the M6/6 procedure only.
var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength);
measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength,
measure.LeanLengthList, dynamicConvention); measure.LeanLengthList, dynamicConvention, measure.MinLengths);
} }
return new ArrangeResult(measure.LengthList); return new ArrangeResult(measure.LengthList);
@ -370,7 +373,7 @@ namespace Avalonia.Controls.Utils
/// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param> /// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param>
/// <returns>The total desired length of all the * length.</returns> /// <returns>The total desired length of all the * length.</returns>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
private double AggregateAdditionalConventionsForStars( private (List<double>, double) AggregateAdditionalConventionsForStars(
IReadOnlyList<LengthConvention> conventions) IReadOnlyList<LengthConvention> conventions)
{ {
// 1. Determine all one-span column's desired widths or row's desired heights. // 1. Determine all one-span column's desired widths or row's desired heights.
@ -403,7 +406,7 @@ namespace Avalonia.Controls.Utils
lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0);
} }
return lengthList.Sum() - fixedLength; return (lengthList, lengthList.Sum() - fixedLength);
} }
/// <summary> /// <summary>
@ -638,13 +641,14 @@ namespace Avalonia.Controls.Utils
/// Initialize a new instance of <see cref="MeasureResult"/>. /// Initialize a new instance of <see cref="MeasureResult"/>.
/// </summary> /// </summary>
internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength,
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions) IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions, IReadOnlyList<double> minLengths)
{ {
ContainerLength = containerLength; ContainerLength = containerLength;
DesiredLength = desiredLength; DesiredLength = desiredLength;
GreedyDesiredLength = greedyDesiredLength; GreedyDesiredLength = greedyDesiredLength;
LeanLengthList = leanConventions; LeanLengthList = leanConventions;
LengthList = expandedConventions; LengthList = expandedConventions;
MinLengths = minLengths;
} }
/// <summary> /// <summary>
@ -674,6 +678,7 @@ namespace Avalonia.Controls.Utils
/// Gets the length list for each column/row. /// Gets the length list for each column/row.
/// </summary> /// </summary>
public IReadOnlyList<double> LengthList { get; } public IReadOnlyList<double> LengthList { get; }
public IReadOnlyList<double> MinLengths { get; }
} }
/// <summary> /// <summary>

651
src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs

@ -0,0 +1,651 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Shared size scope implementation.
/// Shares the size information between participating grids.
/// An instance of this class is attached to every <see cref="Control"/> that has its
/// IsSharedSizeScope property set to true.
/// </summary>
internal sealed class SharedSizeScopeHost : IDisposable
{
private enum MeasurementState
{
Invalidated,
Measuring,
Cached
}
/// <summary>
/// Class containing the measured rows/columns for a single grid.
/// Monitors changes to the row/column collections as well as the SharedSizeGroup changes
/// for the individual items in those collections.
/// Notifies the <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
/// </summary>
private sealed class MeasurementCache : IDisposable
{
readonly CompositeDisposable _subscriptions;
readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>();
public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged;
public MeasurementCache(Grid grid)
{
Grid = grid;
Results = grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(grid.ColumnDefinitions)
.Select(d => new MeasurementResult(grid, d))
.ToList();
grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged;
grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged;
_subscriptions = new CompositeDisposable(
Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged),
grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged));
}
// method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid
private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
// route to collection changed as a Reset.
DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged)
{
if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup))
{
var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1));
var oldName = result.SizeGroup?.Name;
var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup;
_groupChanged.OnNext((oldName, newName, result));
}
}
private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
int offset = 0;
if (sender is ColumnDefinitions)
offset = Grid.RowDefinitions.Count;
var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>();
var oldItems = e.OldStartingIndex >= 0
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count)
: new List<MeasurementResult>();
void NotifyNewItems()
{
foreach (var item in newItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item));
}
}
void NotifyOldItems()
{
foreach (var item in oldItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item));
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Remove:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
NotifyOldItems();
break;
case NotifyCollectionChangedAction.Move:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, oldItems);
break;
case NotifyCollectionChangedAction.Replace:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyOldItems();
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Reset:
oldItems = Results;
newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(Grid.ColumnDefinitions)
.Select(d => new MeasurementResult(Grid, d))
.ToList();
NotifyOldItems();
NotifyNewItems();
break;
}
}
/// <summary>
/// Updates the Results collection with Grid Measure results.
/// </summary>
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
MeasurementState = MeasurementState.Cached;
for (int i = 0; i < Grid.RowDefinitions.Count; i++)
{
Results[i].MeasuredResult = rowResult.LengthList[i];
Results[i].MinLength = rowResult.MinLengths[i];
}
for (int i = 0; i < Grid.ColumnDefinitions.Count; i++)
{
Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i];
Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i];
}
}
/// <summary>
/// Clears the measurement cache, in preparation for the Measure pass.
/// </summary>
public void InvalidateMeasure()
{
var newItems = new List<MeasurementResult>();
var oldItems = new List<MeasurementResult>();
MeasurementState = MeasurementState.Invalidated;
Results.ForEach(r =>
{
r.MeasuredResult = double.NaN;
r.SizeGroup?.Reset();
});
}
/// <summary>
/// Clears the <see cref="IObservable{T}"/> subscriptions.
/// </summary>
public void Dispose()
{
_subscriptions.Dispose();
_groupChanged.OnCompleted();
}
/// <summary>
/// Gets the <see cref="Grid"/> for which this cache has been created.
/// </summary>
public Grid Grid { get; }
/// <summary>
/// Gets the <see cref="MeasurementState"/> of this cache.
/// </summary>
public MeasurementState MeasurementState { get; private set; }
/// <summary>
/// Gets the list of <see cref="MeasurementResult"/> instances.
/// </summary>
/// <remarks>
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
/// </remarks>
public List<MeasurementResult> Results { get; private set; }
}
/// <summary>
/// Class containing the Measure result for a single Row/Column in a grid.
/// </summary>
private class MeasurementResult
{
public MeasurementResult(Grid owningGrid, DefinitionBase definition)
{
OwningGrid = owningGrid;
Definition = definition;
MeasuredResult = double.NaN;
}
/// <summary>
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
/// </summary>
public DefinitionBase Definition { get; }
/// <summary>
/// Gets or sets the actual result of the Measure operation for this column.
/// </summary>
public double MeasuredResult { get; set; }
/// <summary>
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
/// </summary>
public double MinLength { get; set; }
/// <summary>
/// Gets or sets the <see cref="Group"/> that this result belongs to.
/// </summary>
public Group SizeGroup { get; set; }
/// <summary>
/// Gets the Grid that is the parent of the Row/Column
/// </summary>
public Grid OwningGrid { get; }
/// <summary>
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
/// </summary>
/// <returns>A tuple of length and the priority in the shared size group.</returns>
public (double length, int priority) GetPriorityLength()
{
var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height;
if (length.IsAbsolute)
return (MeasuredResult, 1);
if (length.IsAuto)
return (MeasuredResult, 2);
if (MinLength > 0)
return (MinLength, 3);
return (MeasuredResult, 4);
}
}
/// <summary>
/// Visitor class used to gather the final length for a given SharedSizeGroup.
/// </summary>
/// <remarks>
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
/// </remarks>
private class LentgthGatherer
{
/// <summary>
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
/// </summary>
public double Length { get; private set; }
private int gatheredPriority = 6;
/// <summary>
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
public void Visit(MeasurementResult result)
{
var (length, priority) = result.GetPriorityLength();
if (gatheredPriority < priority)
return;
gatheredPriority = priority;
if (gatheredPriority == priority)
{
Length = Math.Max(length,Length);
}
else
{
Length = length;
}
}
}
/// <summary>
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
/// </summary>
private class Group
{
private double? cachedResult;
private List<MeasurementResult> _results = new List<MeasurementResult>();
/// <summary>
/// Gets the name of the SharedSizeGroup.
/// </summary>
public string Name { get; }
public Group(string name)
{
Name = name;
}
/// <summary>
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
/// </summary>
public IReadOnlyList<MeasurementResult> Results => _results;
/// <summary>
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
/// </summary>
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value;
/// <summary>
/// Clears the previously cached result in preparation for measurement.
/// </summary>
public void Reset()
{
cachedResult = null;
}
/// <summary>
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
/// to this instance.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
public void Add(MeasurementResult result)
{
if (_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result");
result.SizeGroup = this;
_results.Add(result);
}
/// <summary>
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
public void Remove(MeasurementResult result)
{
if (!_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result");
result.SizeGroup = null;
_results.Remove(result);
}
private double Gather()
{
var visitor = new LentgthGatherer();
_results.ForEach(visitor.Visit);
return visitor.Length;
}
}
private readonly AvaloniaList<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>();
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>();
private bool _invalidating;
/// <summary>
/// Removes the SharedSizeScope and notifies all affected grids of the change.
/// </summary>
public void Dispose()
{
while (_measurementCaches.Any())
_measurementCaches[0].Grid.SharedScopeChanged();
}
/// <summary>
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
/// </summary>
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
internal void RegisterGrid(Grid toAdd)
{
if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd)))
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!");
var cache = new MeasurementCache(toAdd);
_measurementCaches.Add(cache);
AddGridToScopes(cache);
}
/// <summary>
/// Removes the registration for a grid in this SharedSizeScope.
/// </summary>
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
internal void UnegisterGrid(Grid toRemove)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!");
_measurementCaches.Remove(cache);
RemoveGridFromScopes(cache);
cache.Dispose();
}
/// <summary>
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
/// </summary>
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
/// <returns>True if the grid should forward its calls.</returns>
internal bool ParticipatesInScope(Grid toCheck)
{
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))
?.Results.Any(r => r.SizeGroup != null) ?? false;
}
/// <summary>
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
/// Forwards the same call to all affected grids in this scope.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
internal void InvalidateMeasure(Grid grid)
{
// prevent stack overflow
if (_invalidating)
return;
_invalidating = true;
InvalidateMeasureImpl(grid);
_invalidating = false;
}
/// <summary>
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!");
cache.UpdateMeasureResult(rowResult, columnResult);
}
/// <summary>
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
return (
Arrange(grid.RowDefinitions, rowResult),
Arrange(grid.ColumnDefinitions, columnResult)
);
}
/// <summary>
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
private void InvalidateMeasureImpl(Grid grid)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache");
// already invalidated the cache, early out.
if (cache.MeasurementState == MeasurementState.Invalidated)
return;
// we won't calculate, so we should not invalidate.
if (!ParticipatesInScope(grid))
return;
cache.InvalidateMeasure();
// maybe there is a condition to only call arrange on some of the calls?
grid.InvalidateMeasure();
// find all the scopes within the invalidated grid
var scopeNames = cache.Results
.Where(mr => mr.SizeGroup != null)
.Select(mr => mr.SizeGroup.Name)
.Distinct();
// find all grids related to those scopes
var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results)
.Select(r => r.OwningGrid)
.Where(g => g.IsMeasureValid)
.Distinct();
// invalidate them as well
foreach (var otherGrid in otherGrids)
{
InvalidateMeasureImpl(otherGrid);
}
}
/// <summary>
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
/// SharedSizeGroup
/// </summary>
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change)
{
RemoveFromGroup(change.oldName, change.result);
AddToGroup(change.newName, change.result);
}
/// <summary>
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
/// </summary>
/// <param name="definitions">Rows/Columns that were measured</param>
/// <param name="measureResult">The initial measurement result.</param>
/// <returns>Modified measure result</returns>
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult)
{
var conventions = measureResult.LeanLengthList.ToList();
var lengths = measureResult.LengthList.ToList();
var desiredLength = 0.0;
for (int i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
// for empty SharedSizeGroups pass on unmodified result.
if (string.IsNullOrEmpty(definition.SharedSizeGroup))
{
desiredLength += measureResult.LengthList[i];
continue;
}
var group = _groups[definition.SharedSizeGroup];
// Length calculated over all Definitions participating in a SharedSizeGroup.
var length = group.CalculatedLength;
conventions[i] = new GridLayout.LengthConvention(
new GridLength(length),
measureResult.LeanLengthList[i].MinLength,
measureResult.LeanLengthList[i].MaxLength
);
lengths[i] = length;
desiredLength += length;
}
return new GridLayout.MeasureResult(
measureResult.ContainerLength,
desiredLength,
measureResult.GreedyDesiredLength,//??
conventions,
lengths,
measureResult.MinLengths);
}
/// <summary>
/// Adds all measurement results for a grid to their repsective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
private void AddGridToScopes(MeasurementCache cache)
{
cache.GroupChanged.Subscribe(SharedGroupChanged);
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
AddToGroup(scopeName, result);
}
}
/// <summary>
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
private void AddToGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
_groups.Add(scopeName, group = new Group(scopeName));
group.Add(result);
}
/// <summary>
/// Removes all measurement results for a grid from their respective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
private void RemoveGridFromScopes(MeasurementCache cache)
{
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
RemoveFromGroup(scopeName, result);
}
}
/// <summary>
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
private void RemoveFromGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope");
group.Remove(result);
if (!group.Results.Any())
_groups.Remove(scopeName);
}
}
}

38
src/Avalonia.Controls/Window.cs

@ -397,11 +397,23 @@ namespace Avalonia.Controls
/// <returns> /// <returns>
/// A task that can be used to track the lifetime of the dialog. /// A task that can be used to track the lifetime of the dialog.
/// </returns> /// </returns>
public Task ShowDialog() public Task ShowDialog(Window parent)
{ {
return ShowDialog<object>(); return ShowDialog<object>(parent);
} }
/// <summary>
/// Shows the window as a dialog.
/// </summary>
/// <typeparam name="TResult">
/// The type of the result produced by the dialog.
/// </typeparam>
/// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns>
public Task<TResult> ShowDialog<TResult>(Window parent) => ShowDialog<TResult>(parent.PlatformImpl);
/// <summary> /// <summary>
/// Shows the window as a dialog. /// Shows the window as a dialog.
/// </summary> /// </summary>
@ -411,8 +423,10 @@ namespace Avalonia.Controls
/// <returns>. /// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes. /// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns> /// </returns>
public Task<TResult> ShowDialog<TResult>() public Task<TResult> ShowDialog<TResult>(IWindowImpl parent)
{ {
if(parent == null)
throw new ArgumentNullException(nameof(parent));
if (IsVisible) if (IsVisible)
{ {
throw new InvalidOperationException("The window is already being shown."); throw new InvalidOperationException("The window is already being shown.");
@ -427,24 +441,18 @@ namespace Avalonia.Controls
using (BeginAutoSizing()) using (BeginAutoSizing())
{ {
var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList();
var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault();
SetIsEnabled(affectedWindows, false);
var modal = PlatformImpl?.ShowDialog(); PlatformImpl?.ShowDialog(parent);
var result = new TaskCompletionSource<TResult>(); var result = new TaskCompletionSource<TResult>();
Renderer?.Start(); Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>( Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x, x => this.Closed += x,
x => this.Closed -= x) x => this.Closed -= x)
.Take(1) .Take(1)
.Subscribe(_ => .Subscribe(_ =>
{ {
modal?.Dispose(); parent.Activate();
SetIsEnabled(affectedWindows, true);
activated?.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult))); result.SetResult((TResult)(_dialogResult ?? default(TResult)));
}); });
@ -452,14 +460,6 @@ namespace Avalonia.Controls
} }
} }
void SetIsEnabled(IEnumerable<Window> windows, bool isEnabled)
{
foreach (var window in windows)
{
window.IsEnabled = isEnabled;
}
}
void SetWindowStartupLocation() void SetWindowStartupLocation()
{ {
if (WindowStartupLocation == WindowStartupLocation.CenterScreen) if (WindowStartupLocation == WindowStartupLocation.CenterScreen)

5
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -86,9 +86,8 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public IDisposable ShowDialog() public void ShowDialog(IWindowImpl parent)
{ {
return Disposable.Empty;
} }
public void SetSystemDecorations(bool enabled) public void SetSystemDecorations(bool enabled)
@ -111,4 +110,4 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
} }
} }

6
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -87,7 +87,9 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public IDisposable ShowDialog() => Disposable.Empty; public void ShowDialog(IWindowImpl parent)
{
}
public void SetSystemDecorations(bool enabled) public void SetSystemDecorations(bool enabled)
{ {
@ -157,4 +159,4 @@ namespace Avalonia.DesignerSupport.Remote
public Screen[] AllScreens { get; } = public Screen[] AllScreens { get; } =
{new Screen(new Rect(0, 0, 4000, 4000), new Rect(0, 0, 4000, 4000), true)}; {new Screen(new Rect(0, 0, 4000, 4000), new Rect(0, 0, 4000, 4000), true)};
} }
} }

3
src/Avalonia.Input/InputExtensions.cs

@ -45,7 +45,8 @@ namespace Avalonia.Input
return element != null && return element != null &&
element.IsVisible && element.IsVisible &&
element.IsHitTestVisible && element.IsHitTestVisible &&
element.IsEnabledCore; element.IsEnabledCore &&
element.IsAttachedToVisualTree;
} }
} }
} }

6
src/Avalonia.Layout/Properties/AssemblyInfo.cs

@ -0,0 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]

23
src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs

@ -0,0 +1,23 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
namespace Avalonia.Native
{
public class AvaloniaNativeDeferredRendererLock : IDeferredRendererLock
{
private readonly IAvnWindowBase _window;
public AvaloniaNativeDeferredRendererLock(IAvnWindowBase window)
{
_window = window;
}
public IDisposable TryLock()
{
if (_window.TryLock())
return Disposable.Create(() => _window.Unlock());
return null;
}
}
}

108
src/Avalonia.Native/DeferredRendererProxy.cs

@ -1,108 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia.Native
{
public class DeferredRendererProxy : IRenderer, IRenderLoopTask, IRenderLoop
{
public DeferredRendererProxy(IRenderRoot root, IAvnWindowBase window)
{
if (window != null)
{
_useLock = true;
window.AddRef();
_window = new IAvnWindowBase(window.NativePointer);
}
_renderer = new DeferredRenderer(root, this);
_rendererTask = (IRenderLoopTask)_renderer;
}
void IRenderLoop.Add(IRenderLoopTask i)
{
AvaloniaLocator.Current.GetService<IRenderLoop>().Add(this);
}
void IRenderLoop.Remove(IRenderLoopTask i)
{
AvaloniaLocator.Current.GetService<IRenderLoop>().Remove(this);
}
private DeferredRenderer _renderer;
private IRenderLoopTask _rendererTask;
private IAvnWindowBase _window;
private bool _useLock;
public bool DrawFps{
get => _renderer.DrawFps;
set => _renderer.DrawFps = value;
}
public bool DrawDirtyRects
{
get => _renderer.DrawDirtyRects;
set => _renderer.DrawDirtyRects = value;
}
public bool NeedsUpdate => _rendererTask.NeedsUpdate;
public void AddDirty(IVisual visual) => _renderer.AddDirty(visual);
public void Dispose()
{
_renderer.Dispose();
_window?.Dispose();
_window = null;
}
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
return _renderer.HitTest(p, root, filter);
}
public void Paint(Rect rect)
{
if (NeedsUpdate)
{
Update(TimeSpan.FromMilliseconds(Environment.TickCount));
}
Render();
}
public void Resized(Size size) => _renderer.Resized(size);
public void Start() => _renderer.Start();
public void Stop() => _renderer.Stop();
public void Update(TimeSpan time)
{
_rendererTask.Update(time);
}
public void Render()
{
if(_useLock)
{
_rendererTask.Render();
return;
}
if (_window == null)
return;
if (!_window.TryLock())
return;
try
{
_rendererTask.Render();
}
finally
{
_window.Unlock();
}
}
}
}

4
src/Avalonia.Native/GlPlatformFeature.cs

@ -58,10 +58,8 @@ namespace Avalonia.Native
public IGlDisplay Display { get; } public IGlDisplay Display { get; }
public void MakeCurrent(IGlSurface surface) public void MakeCurrent()
{ {
if (surface != null)
throw new ArgumentException(nameof(surface));
Context.MakeCurrent(); Context.MakeCurrent();
} }
} }

4
src/Avalonia.Native/WindowImpl.cs

@ -47,9 +47,9 @@ namespace Avalonia.Native
public IAvnWindow Native => _native; public IAvnWindow Native => _native;
public IDisposable ShowDialog() public void ShowDialog(IWindowImpl window)
{ {
return _native.ShowDialog(); _native.ShowDialog(((WindowImpl)window).Native);
} }
public void CanResize(bool value) public void CanResize(bool value)

8
src/Avalonia.Native/WindowImplBase.cs

@ -186,10 +186,6 @@ namespace Avalonia.Native
void IAvnWindowBaseEvents.RunRenderPriorityJobs() void IAvnWindowBaseEvents.RunRenderPriorityJobs()
{ {
if (_parent._deferredRendering
&& _parent._lastRenderedLogicalSize != _parent.ClientSize)
// Hack to trigger Paint event on the renderer
_parent.Paint?.Invoke(new Rect());
Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); Dispatcher.UIThread.RunJobs(DispatcherPriority.Render);
} }
} }
@ -245,7 +241,9 @@ namespace Avalonia.Native
public IRenderer CreateRenderer(IRenderRoot root) public IRenderer CreateRenderer(IRenderRoot root)
{ {
if (_deferredRendering) if (_deferredRendering)
return new DeferredRendererProxy(root, _gpu ? _native : null); return new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>(),
rendererLock:
_gpu ? new AvaloniaNativeDeferredRendererLock(_native) : null);
return new ImmediateRenderer(root); return new ImmediateRenderer(root);
} }

44
src/Avalonia.OpenGL/EglContext.cs

@ -0,0 +1,44 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
namespace Avalonia.OpenGL
{
public class EglContext : IGlContext
{
private readonly EglDisplay _disp;
private readonly EglInterface _egl;
private readonly object _lock = new object();
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface)
{
_disp = display;
_egl = egl;
Context = ctx;
OffscreenSurface = offscreenSurface;
}
public IntPtr Context { get; }
public IntPtr OffscreenSurface { get; }
public IGlDisplay Display => _disp;
public IDisposable Lock()
{
Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock));
}
public void MakeCurrent()
{
if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context))
throw new OpenGlException("eglMakeCurrent failed");
}
public void MakeCurrent(EglSurface surface)
{
var surf = ((EglSurface)surface)?.DangerousGetHandle() ?? OffscreenSurface;
if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context))
throw new OpenGlException("eglMakeCurrent failed");
}
}
}

53
src/Avalonia.OpenGL/EglDisplay.cs

@ -12,7 +12,8 @@ namespace Avalonia.OpenGL
private readonly IntPtr _display; private readonly IntPtr _display;
private readonly IntPtr _config; private readonly IntPtr _config;
private readonly int[] _contextAttributes; private readonly int[] _contextAttributes;
public IntPtr Handle => _display;
public EglDisplay(EglInterface egl) public EglDisplay(EglInterface egl)
{ {
_egl = egl; _egl = egl;
@ -121,7 +122,7 @@ namespace Avalonia.OpenGL
}); });
if (surf == IntPtr.Zero) if (surf == IntPtr.Zero)
throw new OpenGlException("eglCreatePbufferSurface failed"); throw new OpenGlException("eglCreatePbufferSurface failed");
var rv = new EglContext(this, ctx, surf); var rv = new EglContext(this, _egl, ctx, surf);
rv.MakeCurrent(null); rv.MakeCurrent(null);
return rv; return rv;
} }
@ -132,12 +133,12 @@ namespace Avalonia.OpenGL
throw new OpenGlException("eglMakeCurrent failed"); throw new OpenGlException("eglMakeCurrent failed");
} }
public IGlSurface CreateWindowSurface(IntPtr window) public EglSurface CreateWindowSurface(IntPtr window)
{ {
var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE}); var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE});
if (s == IntPtr.Zero) if (s == IntPtr.Zero)
throw new OpenGlException("eglCreateWindowSurface failed"); throw new OpenGlException("eglCreateWindowSurface failed");
return new EglSurface(this, s); return new EglSurface(this, _egl, s);
} }
public int SampleCount public int SampleCount
@ -157,49 +158,5 @@ namespace Avalonia.OpenGL
return rv; return rv;
} }
} }
class EglSurface : SafeHandle, IGlSurface
{
private readonly EglDisplay _display;
public EglSurface(EglDisplay display, IntPtr surface) : base(surface, true)
{
_display = display;
}
protected override bool ReleaseHandle()
{
_display._egl.DestroySurface(_display._display, handle);
return true;
}
public override bool IsInvalid => handle == IntPtr.Zero;
public IGlDisplay Display => _display;
public void SwapBuffers() => _display._egl.SwapBuffers(_display._display, handle);
}
class EglContext : IGlContext
{
private readonly EglDisplay _disp;
public EglContext(EglDisplay display, IntPtr ctx, IntPtr offscreenSurface)
{
_disp = display;
Context = ctx;
OffscreenSurface = offscreenSurface;
}
public IntPtr Context { get; }
public IntPtr OffscreenSurface { get; }
public IGlDisplay Display => _disp;
public void MakeCurrent(IGlSurface surface)
{
var surf = ((EglSurface)surface)?.DangerousGetHandle() ?? OffscreenSurface;
if (!_disp._egl.MakeCurrent(_disp._display, surf, surf, Context))
throw new OpenGlException("eglMakeCurrent failed");
}
}
} }
} }

4
src/Avalonia.OpenGL/EglGlPlatformFeature.cs

@ -7,7 +7,7 @@ namespace Avalonia.OpenGL
{ {
public IGlDisplay Display { get; set; } public IGlDisplay Display { get; set; }
public IGlContext ImmediateContext { get; set; } public IGlContext ImmediateContext { get; set; }
public IGlContext DeferredContext { get; set; } public EglContext DeferredContext { get; set; }
public static void TryInitialize() public static void TryInitialize()
{ {
@ -26,7 +26,7 @@ namespace Avalonia.OpenGL
{ {
Display = disp, Display = disp,
ImmediateContext = ctx, ImmediateContext = ctx,
DeferredContext = disp.CreateContext(ctx) DeferredContext = (EglContext)disp.CreateContext(ctx)
}; };
} }
catch(Exception e) catch(Exception e)

32
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
@ -12,10 +13,10 @@ namespace Avalonia.OpenGL
} }
private readonly EglDisplay _display; private readonly EglDisplay _display;
private readonly IGlContext _context; private readonly EglContext _context;
private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly IEglWindowGlPlatformSurfaceInfo _info;
public EglGlPlatformSurface(EglDisplay display, IGlContext context, IEglWindowGlPlatformSurfaceInfo info) public EglGlPlatformSurface(EglDisplay display, EglContext context, IEglWindowGlPlatformSurfaceInfo info)
{ {
_display = display; _display = display;
_context = context; _context = context;
@ -30,11 +31,11 @@ namespace Avalonia.OpenGL
class RenderTarget : IGlPlatformSurfaceRenderTarget class RenderTarget : IGlPlatformSurfaceRenderTarget
{ {
private readonly IGlContext _context; private readonly EglContext _context;
private readonly IGlSurface _glSurface; private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly IEglWindowGlPlatformSurfaceInfo _info;
public RenderTarget(IGlContext context, IGlSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
{ {
_context = context; _context = context;
_glSurface = glSurface; _glSurface = glSurface;
@ -45,21 +46,33 @@ namespace Avalonia.OpenGL
public IGlPlatformSurfaceRenderingSession BeginDraw() public IGlPlatformSurfaceRenderingSession BeginDraw()
{ {
_context.MakeCurrent(_glSurface); var l = _context.Lock();
return new Session(_context, _glSurface, _info); try
{
_context.MakeCurrent(_glSurface);
return new Session(_context, _glSurface, _info, l);
}
catch
{
l.Dispose();
throw;
}
} }
class Session : IGlPlatformSurfaceRenderingSession class Session : IGlPlatformSurfaceRenderingSession
{ {
private readonly IGlContext _context; private readonly IGlContext _context;
private readonly IGlSurface _glSurface; private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly IEglWindowGlPlatformSurfaceInfo _info;
private IDisposable _lock;
public Session(IGlContext context, IGlSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock)
{ {
_context = context; _context = context;
_glSurface = glSurface; _glSurface = glSurface;
_info = info; _info = info;
_lock = @lock;
} }
public void Dispose() public void Dispose()
@ -67,6 +80,7 @@ namespace Avalonia.OpenGL
_context.Display.GlInterface.Flush(); _context.Display.GlInterface.Flush();
_glSurface.SwapBuffers(); _glSurface.SwapBuffers();
_context.Display.ClearContext(); _context.Display.ClearContext();
_lock.Dispose();
} }
public IGlDisplay Display => _context.Display; public IGlDisplay Display => _context.Display;

28
src/Avalonia.OpenGL/EglSurface.cs

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.OpenGL
{
public class EglSurface : SafeHandle
{
private readonly EglDisplay _display;
private readonly EglInterface _egl;
public EglSurface(EglDisplay display, EglInterface egl, IntPtr surface) : base(surface, true)
{
_display = display;
_egl = egl;
}
protected override bool ReleaseHandle()
{
_egl.DestroySurface(_display.Handle, handle);
return true;
}
public override bool IsInvalid => handle == IntPtr.Zero;
public IGlDisplay Display => _display;
public void SwapBuffers() => _egl.SwapBuffers(_display.Handle, handle);
}
}

4
src/Avalonia.OpenGL/IGlContext.cs

@ -3,6 +3,6 @@ namespace Avalonia.OpenGL
public interface IGlContext public interface IGlContext
{ {
IGlDisplay Display { get; } IGlDisplay Display { get; }
void MakeCurrent(IGlSurface surface); void MakeCurrent();
} }
} }

10
src/Avalonia.OpenGL/IGlSurface.cs

@ -1,10 +0,0 @@
using System;
namespace Avalonia.OpenGL
{
public interface IGlSurface : IDisposable
{
IGlDisplay Display { get; }
void SwapBuffers();
}
}

12
src/Avalonia.Styling/StyledElement.cs

@ -49,8 +49,11 @@ namespace Avalonia
/// <summary> /// <summary>
/// Defines the <see cref="TemplatedParent"/> property. /// Defines the <see cref="TemplatedParent"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ITemplatedControl> TemplatedParentProperty = public static readonly DirectProperty<StyledElement, ITemplatedControl> TemplatedParentProperty =
AvaloniaProperty.Register<StyledElement, ITemplatedControl>(nameof(TemplatedParent), inherits: true); AvaloniaProperty.RegisterDirect<StyledElement, ITemplatedControl>(
nameof(TemplatedParent),
o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v);
private int _initCount; private int _initCount;
private string _name; private string _name;
@ -62,6 +65,7 @@ namespace Avalonia
private Styles _styles; private Styles _styles;
private bool _styled; private bool _styled;
private Subject<IStyleable> _styleDetach = new Subject<IStyleable>(); private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
private ITemplatedControl _templatedParent;
private bool _dataContextUpdating; private bool _dataContextUpdating;
/// <summary> /// <summary>
@ -269,8 +273,8 @@ namespace Avalonia
/// </summary> /// </summary>
public ITemplatedControl TemplatedParent public ITemplatedControl TemplatedParent
{ {
get { return GetValue(TemplatedParentProperty); } get => _templatedParent;
internal set { SetValue(TemplatedParentProperty, value); } internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
} }
/// <summary> /// <summary>

2
src/Avalonia.Themes.Default/Accents/BaseDark.xaml

@ -52,5 +52,7 @@
<sys:Double x:Key="FontSizeSmall">10</sys:Double> <sys:Double x:Key="FontSizeSmall">10</sys:Double>
<sys:Double x:Key="FontSizeNormal">12</sys:Double> <sys:Double x:Key="FontSizeNormal">12</sys:Double>
<sys:Double x:Key="FontSizeLarge">16</sys:Double> <sys:Double x:Key="FontSizeLarge">16</sys:Double>
<sys:Double x:Key="ScrollBarThickness">10</sys:Double>
</Style.Resources> </Style.Resources>
</Style> </Style>

2
src/Avalonia.Themes.Default/Accents/BaseLight.xaml

@ -52,5 +52,7 @@
<sys:Double x:Key="FontSizeSmall">10</sys:Double> <sys:Double x:Key="FontSizeSmall">10</sys:Double>
<sys:Double x:Key="FontSizeNormal">12</sys:Double> <sys:Double x:Key="FontSizeNormal">12</sys:Double>
<sys:Double x:Key="FontSizeLarge">16</sys:Double> <sys:Double x:Key="FontSizeLarge">16</sys:Double>
<sys:Double x:Key="ScrollBarThickness">10</sys:Double>
</Style.Resources> </Style.Resources>
</Style> </Style>

27
src/Avalonia.Themes.Default/ButtonSpinner.xaml

@ -1,17 +1,17 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Style Selector="ButtonSpinner"> <Style Selector="ButtonSpinner">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/>
</Style> </Style>
<Style Selector="ButtonSpinner /template/ RepeatButton"> <Style Selector="ButtonSpinner /template/ RepeatButton">
<Setter Property="RepeatButton.Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="RepeatButton.BorderBrush" Value="Transparent"/> <Setter Property="BorderBrush" Value="Transparent"/>
</Style> </Style>
<Style Selector="ButtonSpinner /template/ RepeatButton:pointerover"> <Style Selector="ButtonSpinner /template/ RepeatButton:pointerover">
<Setter Property="RepeatButton.Background" Value="{DynamicResource ThemeControlMidBrush}"/> <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="RepeatButton.BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
</Style> </Style>
<Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton"> <Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton">
<Setter Property="Content"> <Setter Property="Content">
@ -42,7 +42,8 @@
<Style Selector="ButtonSpinner:right"> <Style Selector="ButtonSpinner:right">
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}" <Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
@ -67,7 +68,8 @@
<Style Selector="ButtonSpinner:left"> <Style Selector="ButtonSpinner:left">
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}" <Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
@ -89,4 +91,13 @@
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
</Styles> <Style Selector="ButtonSpinner:pointerover /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
</Style>
<Style Selector="ButtonSpinner:focus /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
</Style>
<Style Selector="ButtonSpinner:error /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
</Style>
</Styles>

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -29,6 +29,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.TabStripItem.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.TabStripItem.xaml?assembly=Avalonia.Themes.Default"/>
<!-- TabControl needs to come after TabStrip as it redefines the inner TabStrip.ItemsPanel--> <!-- TabControl needs to come after TabStrip as it redefines the inner TabStrip.ItemsPanel-->
<StyleInclude Source="resm:Avalonia.Themes.Default.TabControl.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.TabControl.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TabItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.TextBox.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.TextBox.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ToggleButton.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.ToggleButton.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Expander.xaml?assembly=Avalonia.Themes.Default"/> <StyleInclude Source="resm:Avalonia.Themes.Default.Expander.xaml?assembly=Avalonia.Themes.Default"/>

29
src/Avalonia.Themes.Default/NumericUpDown.xaml

@ -1,10 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui"> <Styles xmlns="https://github.com/avaloniaui">
<Style Selector="NumericUpDown"> <Style Selector="NumericUpDown">
<Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLowBrush}"/> <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" /> <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="TemplatedControl.Foreground" Value="{DynamicResource ThemeForegroundBrush}" /> <Setter Property="Padding" Value="4"/>
<Setter Property="TemplatedControl.Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<ButtonSpinner Name="PART_Spinner" <ButtonSpinner Name="PART_Spinner"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
@ -17,20 +17,13 @@
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}"> ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}">
<TextBox Name="PART_TextBox" <TextBox Name="PART_TextBox"
BorderThickness="0" BorderThickness="0"
Background="Transparent" Background="{TemplateBinding Background}"
ContextMenu="{TemplateBinding ContextMenu}" BorderBrush="{TemplateBinding BorderBrush}"
FontFamily="{TemplateBinding FontFamily}" Padding="{TemplateBinding Padding}"
FontSize="{TemplateBinding FontSize}"
FontStyle="{TemplateBinding FontStyle}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
Watermark="{TemplateBinding Watermark}" Watermark="{TemplateBinding Watermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
IsReadOnly="{TemplateBinding IsReadOnly}" IsReadOnly="{TemplateBinding IsReadOnly}"
Text="{TemplateBinding Text}" Text="{TemplateBinding Text}"
Padding="{TemplateBinding Padding}"
TextAlignment="Left"
Margin="1"
MinWidth="20"
AcceptsReturn="False" AcceptsReturn="False"
TextWrapping="NoWrap"> TextWrapping="NoWrap">
</TextBox> </TextBox>
@ -38,4 +31,8 @@
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>
<Style Selector="NumericUpDown /template/ TextBox#PART_TextBox">
<Setter Property="Margin" Value="4"/>
<Setter Property="MinWidth" Value="20"/>
</Style>
</Styles> </Styles>

22
src/Avalonia.Themes.Default/ScrollBar.xaml

@ -3,7 +3,7 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{DynamicResource ThemeControlMidBrush}"> <Border Background="{DynamicResource ThemeControlMidBrush}">
<Grid RowDefinitions="10,*,10"> <Grid RowDefinitions="Auto,*,Auto">
<RepeatButton Name="PART_LineUpButton" <RepeatButton Name="PART_LineUpButton"
Classes="repeat" Classes="repeat"
Grid.Row="0" Grid.Row="0"
@ -53,12 +53,11 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="ScrollBar:horizontal"> <Style Selector="ScrollBar:horizontal">
<Setter Property="Height" <Setter Property="Height" Value="{DynamicResource ScrollBarThickness}" />
Value="10" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Background="{DynamicResource ThemeControlMidBrush}"> <Border Background="{DynamicResource ThemeControlMidBrush}">
<Grid ColumnDefinitions="10,*,10"> <Grid ColumnDefinitions="Auto,*,Auto">
<RepeatButton Name="PART_LineUpButton" <RepeatButton Name="PART_LineUpButton"
Classes="repeat" Classes="repeat"
Grid.Row="0" Grid.Row="0"
@ -108,22 +107,17 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="ScrollBar:horizontal /template/ Thumb#thumb"> <Style Selector="ScrollBar:horizontal /template/ Thumb#thumb">
<Setter Property="MinWidth" <Setter Property="MinWidth" Value="{DynamicResource ScrollBarThickness}" />
Value="10" />
</Style> </Style>
<Style Selector="ScrollBar:vertical"> <Style Selector="ScrollBar:vertical">
<Setter Property="Width" <Setter Property="Width" Value="{DynamicResource ScrollBarThickness}" />
Value="10" />
</Style> </Style>
<Style Selector="ScrollBar:vertical /template/ Thumb#thumb"> <Style Selector="ScrollBar:vertical /template/ Thumb#thumb">
<Setter Property="MinHeight" <Setter Property="MinHeight" Value="{DynamicResource ScrollBarThickness}" />
Value="10" />
</Style> </Style>
<Style Selector="ScrollBar /template/ RepeatButton.repeat"> <Style Selector="ScrollBar /template/ RepeatButton.repeat">
<Setter Property="Padding" <Setter Property="Padding" Value="2" />
Value="2" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderThickness"
Value="0" />
</Style> </Style>
<Style Selector="ScrollBar /template/ RepeatButton.repeattrack"> <Style Selector="ScrollBar /template/ RepeatButton.repeattrack">
<Setter Property="Template"> <Setter Property="Template">

104
src/Avalonia.Themes.Default/TabControl.xaml

@ -1,50 +1,56 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="TabControl"> <Style Selector="TabControl">
<Setter Property="Template"> <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<ControlTemplate> <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Border Background="{TemplateBinding Background}" <Setter Property="Padding" Value="4"/>
BorderBrush="{TemplateBinding BorderBrush}" <Setter Property="VerticalContentAlignment" Value="Stretch"/>
BorderThickness="{TemplateBinding BorderThickness}"> <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<DockPanel> <Setter Property="Template">
<TabStrip Name="PART_TabStrip" <ControlTemplate>
MemberSelector="{x:Static TabControl.HeaderSelector}" <Border
Items="{TemplateBinding Items}" Margin="{TemplateBinding Margin}"
SelectedIndex="{TemplateBinding SelectedIndex, Mode=TwoWay}"/> BorderBrush="{TemplateBinding BorderBrush}"
<Carousel Name="PART_Content" BorderThickness="{TemplateBinding BorderThickness}"
MemberSelector="{x:Static TabControl.ContentSelector}" Background="{TemplateBinding Background}"
Items="{TemplateBinding Items}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
SelectedIndex="{TemplateBinding SelectedIndex}" VerticalAlignment="{TemplateBinding VerticalAlignment}">
PageTransition="{TemplateBinding PageTransition}" <DockPanel>
Grid.Row="1"/> <ItemsPresenter
</DockPanel> Name="PART_ItemsPresenter"
</Border> Items="{TemplateBinding Items}"
</ControlTemplate> ItemsPanel="{TemplateBinding ItemsPanel}"
</Setter> ItemTemplate="{TemplateBinding ItemTemplate}"
</Style> MemberSelector="{TemplateBinding MemberSelector}" >
<Style Selector="TabControl[TabStripPlacement=Top] /template/ TabStrip"> </ItemsPresenter>
<Setter Property="DockPanel.Dock" Value="Top"/> <ContentPresenter
</Style> Name="PART_Content"
<Style Selector="TabControl[TabStripPlacement=Bottom] /template/ TabStrip"> Margin="{TemplateBinding Padding}"
<Setter Property="DockPanel.Dock" Value="Bottom"/> HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
</Style> VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
<Style Selector="TabControl[TabStripPlacement=Left] /template/ TabStrip"> Content="{TemplateBinding SelectedContent}"
<Setter Property="DockPanel.Dock" Value="Left"/> ContentTemplate="{TemplateBinding SelectedContentTemplate}">
<Setter Property="ItemsPanel"> </ContentPresenter>
<Setter.Value> </DockPanel>
<ItemsPanelTemplate> </Border>
<StackPanel Orientation="Vertical"/> </ControlTemplate>
</ItemsPanelTemplate> </Setter>
</Setter.Value> </Style>
</Setter> <Style Selector="TabControl[TabStripPlacement=Top] /template/ ItemsPresenter#PART_ItemsPresenter">
</Style> <Setter Property="DockPanel.Dock" Value="Top"/>
<Style Selector="TabControl[TabStripPlacement=Right] /template/ TabStrip"> </Style>
<Setter Property="DockPanel.Dock" Value="Right"/> <Style Selector="TabControl[TabStripPlacement=Bottom] /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="ItemsPanel"> <Setter Property="DockPanel.Dock" Value="Bottom"/>
<Setter.Value> </Style>
<ItemsPanelTemplate> <Style Selector="TabControl[TabStripPlacement=Left] /template/ ItemsPresenter#PART_ItemsPresenter">
<StackPanel Orientation="Vertical"/> <Setter Property="DockPanel.Dock" Value="Left"/>
</ItemsPanelTemplate> </Style>
</Setter.Value> <Style Selector="TabControl[TabStripPlacement=Left] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
</Setter> <Setter Property="Orientation" Value="Vertical"/>
</Style> </Style>
</Styles> <Style Selector="TabControl[TabStripPlacement=Right] /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="DockPanel.Dock" Value="Right"/>
</Style>
<Style Selector="TabControl[TabStripPlacement=Right] /template/ ItemsPresenter#PART_ItemsPresenter > WrapPanel">
<Setter Property="Orientation" Value="Vertical"/>
</Style>
</Styles>

39
src/Avalonia.Themes.Default/TabItem.xaml

@ -0,0 +1,39 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="TabItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLightBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter
Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Content="{TemplateBinding Header}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"/>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabItem:disabled">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
<Style Selector="TabItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
</Style>
<Style Selector="TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
</Style>
<Style Selector="TabItem:selected:focus /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/>
</Style>
<Style Selector="TabItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush3}"/>
</Style>
<Style Selector="TabItem:selected:focus:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
</Style>
</Styles>

5
src/Avalonia.Themes.Default/TextBox.xaml

@ -62,4 +62,7 @@
<Style Selector="TextBox:error /template/ Border#border"> <Style Selector="TextBox:error /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
</Style> </Style>
</Styles> <Style Selector="TextBox">
<Setter Property="Cursor" Value="IBeam" />
</Style>
</Styles>

177
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -36,6 +36,8 @@ namespace Avalonia.Rendering
private int _lastSceneId = -1; private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
private IRef<IDrawOperation> _currentDraw; private IRef<IDrawOperation> _currentDraw;
private readonly IDeferredRendererLock _lock;
private readonly object _sceneLock = new object();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class. /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@ -44,11 +46,13 @@ namespace Avalonia.Rendering
/// <param name="renderLoop">The render loop.</param> /// <param name="renderLoop">The render loop.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param> /// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <param name="dispatcher">The dispatcher to use. Optional.</param> /// <param name="dispatcher">The dispatcher to use. Optional.</param>
/// <param name="rendererLock">Lock object used before trying to access render target</param>
public DeferredRenderer( public DeferredRenderer(
IRenderRoot root, IRenderRoot root,
IRenderLoop renderLoop, IRenderLoop renderLoop,
ISceneBuilder sceneBuilder = null, ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null) IDispatcher dispatcher = null,
IDeferredRendererLock rendererLock = null)
{ {
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
@ -57,6 +61,7 @@ namespace Avalonia.Rendering
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); _sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers(); Layers = new RenderLayers();
_renderLoop = renderLoop; _renderLoop = renderLoop;
_lock = rendererLock ?? new ManagedDeferredRendererLock();
} }
/// <summary> /// <summary>
@ -80,6 +85,7 @@ namespace Avalonia.Rendering
RenderTarget = renderTarget; RenderTarget = renderTarget;
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); _sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers(); Layers = new RenderLayers();
_lock = new ManagedDeferredRendererLock();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -114,8 +120,13 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
var scene = Interlocked.Exchange(ref _scene, null); lock (_sceneLock)
scene?.Dispose(); {
var scene = _scene;
_scene = null;
scene?.Dispose();
}
Stop(); Stop();
Layers.Clear(); Layers.Clear();
@ -130,13 +141,18 @@ namespace Avalonia.Rendering
// When unit testing the renderLoop may be null, so update the scene manually. // When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene(); UpdateScene();
} }
//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<IVisual>(); return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<IVisual>();
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Paint(Rect rect) public void Paint(Rect rect)
{ {
var t = (IRenderLoopTask)this;
if(t.NeedsUpdate)
UpdateScene();
Render(true);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -164,17 +180,12 @@ namespace Avalonia.Rendering
} }
} }
bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; bool NeedsUpdate => _dirty == null || _dirty.Count > 0;
bool IRenderLoopTask.NeedsUpdate => NeedsUpdate;
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); void IRenderLoopTask.Update(TimeSpan time) => UpdateScene();
void IRenderLoopTask.Render() void IRenderLoopTask.Render() => Render(false);
{
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
}
}
/// <inheritdoc/> /// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
@ -195,69 +206,105 @@ namespace Avalonia.Rendering
internal void UnitTestUpdateScene() => UpdateScene(); internal void UnitTestUpdateScene() => UpdateScene();
internal void UnitTestRender() => Render(_scene.Item); internal void UnitTestRender() => Render(false);
private void Render(Scene scene) private void Render(bool forceComposite)
{ {
bool renderOverlay = DrawDirtyRects || DrawFps; using (var l = _lock.TryLock())
bool composite = false;
if (RenderTarget == null)
{
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
}
if (renderOverlay)
{ {
_dirtyRectsDisplay.Tick(); if (l == null)
} return;
try IDrawingContextImpl context = null;
{ try
if (scene != null && scene.Size != Size.Empty)
{ {
IDrawingContextImpl context = null; try
if (scene.Generation != _lastSceneId)
{ {
context = RenderTarget.CreateDrawingContext(this); IDrawingContextImpl GetContext()
Layers.Update(scene, context); {
if (context != null)
return context;
if (RenderTarget == null)
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
return context = RenderTarget.CreateDrawingContext(this);
RenderToLayers(scene); }
if (DebugFramesPath != null) var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext);
using (scene)
{ {
SaveDebugFrames(scene.Generation); var overlay = DrawDirtyRects || DrawFps;
if (DrawDirtyRects)
_dirtyRectsDisplay.Tick();
if (overlay)
RenderOverlay(scene.Item, GetContext());
if (updated || forceComposite || overlay)
RenderComposite(scene.Item, GetContext());
} }
}
finally
{
context?.Dispose();
}
}
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
RenderTarget?.Dispose();
RenderTarget = null;
}
}
}
_lastSceneId = scene.Generation; private (IRef<Scene> scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(Func<IDrawingContextImpl> contextFactory,
bool recursiveCall = false)
{
IRef<Scene> sceneRef;
lock (_sceneLock)
sceneRef = _scene?.Clone();
if (sceneRef == null)
return (null, false);
using (sceneRef)
{
var scene = sceneRef.Item;
if (scene.Generation != _lastSceneId)
{
var context = contextFactory();
Layers.Update(scene, context);
composite = true; RenderToLayers(scene);
}
if (renderOverlay) if (DebugFramesPath != null)
{ {
context = context ?? RenderTarget.CreateDrawingContext(this); SaveDebugFrames(scene.Generation);
RenderOverlay(scene, context);
RenderComposite(scene, context);
} }
else if (composite)
lock (_sceneLock)
_lastSceneId = scene.Generation;
// 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)
{ {
context = context ?? RenderTarget.CreateDrawingContext(this); UpdateScene();
RenderComposite(scene, context); var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(contextFactory, true);
return (rs, true);
} }
context?.Dispose(); // Indicate that we have updated the layers
return (sceneRef.Clone(), true);
} }
// Just return scene, layers weren't updated
return (sceneRef.Clone(), false);
} }
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
RenderTarget?.Dispose();
RenderTarget = null;
}
} }
private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds) private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds)
{ {
if (layer == null || node.LayerRoot == layer) if (layer == null || node.LayerRoot == layer)
@ -393,6 +440,11 @@ namespace Avalonia.Rendering
private void UpdateScene() private void UpdateScene()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
lock (_sceneLock)
{
if (_scene?.Item.Generation > _lastSceneId)
return;
}
if (_root.IsVisible) if (_root.IsVisible)
{ {
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
@ -411,16 +463,23 @@ namespace Avalonia.Rendering
} }
} }
var oldScene = Interlocked.Exchange(ref _scene, sceneRef); lock (_sceneLock)
oldScene?.Dispose(); {
var oldScene = _scene;
_scene = sceneRef;
oldScene?.Dispose();
}
_dirty.Clear(); _dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
} }
else else
{ {
var oldScene = Interlocked.Exchange(ref _scene, null); lock (_sceneLock)
oldScene?.Dispose(); {
var oldScene = _scene;
_scene = null;
oldScene?.Dispose();
}
} }
} }

9
src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs

@ -0,0 +1,9 @@
using System;
namespace Avalonia.Rendering
{
public interface IDeferredRendererLock
{
IDisposable TryLock();
}
}

17
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -0,0 +1,17 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
namespace Avalonia.Rendering
{
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return null;
}
}
}

4
src/Gtk/Avalonia.Gtk3/Interop/Native.cs

@ -67,6 +67,9 @@ namespace Avalonia.Gtk3.Interop
[UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
public delegate void gtk_window_set_modal(GtkWindow window, bool modal); public delegate void gtk_window_set_modal(GtkWindow window, bool modal);
[UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
public delegate void gtk_window_set_transient_for(GtkWindow window, IntPtr parent);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] //No manual import [UnmanagedFunctionPointer(CallingConvention.Cdecl)] //No manual import
public delegate IntPtr gdk_get_native_handle(IntPtr gdkWindow); public delegate IntPtr gdk_get_native_handle(IntPtr gdkWindow);
@ -408,6 +411,7 @@ namespace Avalonia.Gtk3.Interop
public static D.gtk_window_new GtkWindowNew; public static D.gtk_window_new GtkWindowNew;
public static D.gtk_window_set_icon GtkWindowSetIcon; public static D.gtk_window_set_icon GtkWindowSetIcon;
public static D.gtk_window_set_modal GtkWindowSetModal; public static D.gtk_window_set_modal GtkWindowSetModal;
public static D.gtk_window_set_transient_for GtkWindowSetTransientFor;
public static D.gdk_set_allowed_backends GdkSetAllowedBackends; public static D.gdk_set_allowed_backends GdkSetAllowedBackends;
public static D.gtk_init GtkInit; public static D.gtk_init GtkInit;
public static D.gtk_window_present GtkWindowPresent; public static D.gtk_window_present GtkWindowPresent;

8
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@ -222,14 +222,14 @@ namespace Avalonia.Gtk3
{ {
var evnt = (GdkEventKey*) pev; var evnt = (GdkEventKey*) pev;
_lastKbdEvent = evnt->time; _lastKbdEvent = evnt->time;
if (Native.GtkImContextFilterKeypress(_imContext, pev))
return true;
var e = new RawKeyEventArgs( var e = new RawKeyEventArgs(
Gtk3Platform.Keyboard, Gtk3Platform.Keyboard,
evnt->time, evnt->time,
evnt->type == GdkEventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, evnt->type == GdkEventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
Avalonia.Gtk.Common.KeyTransform.ConvertKey((GdkKey)evnt->keyval), GetModifierKeys((GdkModifierType)evnt->state)); Avalonia.Gtk.Common.KeyTransform.ConvertKey((GdkKey)evnt->keyval), GetModifierKeys((GdkModifierType)evnt->state));
OnInput(e); OnInput(e);
if (Native.GtkImContextFilterKeypress(_imContext, pev))
return true;
return true; return true;
} }
@ -265,6 +265,8 @@ namespace Avalonia.Gtk3
Paint?.Invoke(new Rect(ClientSize)); Paint?.Invoke(new Rect(ClientSize));
CurrentCairoContext = IntPtr.Zero; CurrentCairoContext = IntPtr.Zero;
} }
else
Paint?.Invoke(new Rect(ClientSize));
return true; return true;
} }
@ -422,7 +424,7 @@ namespace Avalonia.Gtk3
Native.GdkWindowSetCursor(Native.GtkWidgetGetWindow(GtkWidget), cursor?.Handle ?? IntPtr.Zero); Native.GdkWindowSetCursor(Native.GtkWidgetGetWindow(GtkWidget), cursor?.Handle ?? IntPtr.Zero);
} }
public void Show() => Native.GtkWindowPresent(GtkWidget); public virtual void Show() => Native.GtkWindowPresent(GtkWidget);
public void Hide() => Native.GtkWidgetHide(GtkWidget); public void Hide() => Native.GtkWidgetHide(GtkWidget);

13
src/Gtk/Avalonia.Gtk3/WindowImpl.cs

@ -63,11 +63,18 @@ namespace Avalonia.Gtk3
public Action<WindowState> WindowStateChanged { get; set; } public Action<WindowState> WindowStateChanged { get; set; }
public IDisposable ShowDialog() public void ShowDialog(IWindowImpl parent)
{ {
Native.GtkWindowSetModal(GtkWidget, true); Native.GtkWindowSetModal(GtkWidget, true);
Show(); Native.GtkWindowSetTransientFor(GtkWidget, ((WindowImpl)parent).GtkWidget.DangerousGetHandle());
return new EmptyDisposable(); Native.GtkWindowPresent(GtkWidget);
}
public override void Show()
{
Native.GtkWindowSetModal(GtkWidget, false);
Native.GtkWindowSetTransientFor(GtkWidget, IntPtr.Zero);
Native.GtkWindowPresent(GtkWidget);
} }
public void SetSystemDecorations(bool enabled) => Native.GtkWindowSetDecorated(GtkWidget, enabled); public void SetSystemDecorations(bool enabled) => Native.GtkWindowSetDecorated(GtkWidget, enabled);

2
src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.Converters;
@ -36,6 +37,7 @@ namespace Avalonia.Markup.Xaml
{ typeof(Selector), typeof(SelectorTypeConverter) }, { typeof(Selector), typeof(SelectorTypeConverter) },
{ typeof(TimeSpan), typeof(TimeSpanTypeConverter) }, { typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
{ typeof(WindowIcon), typeof(IconTypeConverter) }, { typeof(WindowIcon), typeof(IconTypeConverter) },
{ typeof(CultureInfo), typeof(CultureInfoConverter)}
}; };
/// <summary> /// <summary>

16
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs

@ -1,13 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Data; using Avalonia.Data;
using Portable.Xaml.Markup;
namespace Avalonia.Markup.Xaml.MarkupExtensions namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
using Portable.Xaml.Markup;
using System;
public class RelativeSourceExtension : MarkupExtension public class RelativeSourceExtension : MarkupExtension
{ {
public RelativeSourceExtension() public RelativeSourceExtension()
@ -24,10 +23,19 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return new RelativeSource return new RelativeSource
{ {
Mode = Mode, Mode = Mode,
AncestorType = AncestorType,
AncestorLevel = AncestorLevel,
Tree = Tree,
}; };
} }
[ConstructorArgument("mode")] [ConstructorArgument("mode")]
public RelativeSourceMode Mode { get; set; } public RelativeSourceMode Mode { get; set; } = RelativeSourceMode.FindAncestor;
public Type AncestorType { get; set; }
public TreeType Tree { get; set; }
public int AncestorLevel { get; set; } = 1;
} }
} }

8
src/Shared/PlatformSupport/StandardRuntimePlatform.cs

@ -89,9 +89,11 @@ namespace Avalonia.Shared.PlatformSupport
#if DEBUG #if DEBUG
if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId)
{ {
Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " lock(_lock)
+ Environment.StackTrace if (!IsDisposed)
+ "\n\nBlob created by " + _backtrace); Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: "
+ Environment.StackTrace
+ "\n\nBlob created by " + _backtrace);
} }
#endif #endif
DoDispose(); DoDispose();

2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -28,7 +28,7 @@ namespace Avalonia.Skia
var iface = display.Type == GlDisplayType.OpenGL2 var iface = display.Type == GlDisplayType.OpenGL2
? GRGlInterface.AssembleGlInterface((_, proc) => display.GlInterface.GetProcAddress(proc)) ? GRGlInterface.AssembleGlInterface((_, proc) => display.GlInterface.GetProcAddress(proc))
: GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)); : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc));
gl.ImmediateContext.MakeCurrent(null); gl.ImmediateContext.MakeCurrent();
GrContext = GRContext.Create(GRBackend.OpenGL, iface); GrContext = GRContext.Create(GRBackend.OpenGL, iface);
} }
} }

7
src/Windows/Avalonia.Win32/FramebufferManager.cs

@ -5,7 +5,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
class FramebufferManager : IFramebufferPlatformSurface, IDisposable class FramebufferManager : IFramebufferPlatformSurface
{ {
private readonly IntPtr _hwnd; private readonly IntPtr _hwnd;
private WindowFramebuffer _fb; private WindowFramebuffer _fb;
@ -29,10 +29,5 @@ namespace Avalonia.Win32
} }
return _fb; return _fb;
} }
public void Dispose()
{
_fb?.Deallocate();
}
} }
} }

18
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -780,8 +780,8 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLong")] [DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLong")]
private static extern uint SetWindowLong32b(IntPtr hWnd, int nIndex, uint value); private static extern uint SetWindowLong32b(IntPtr hWnd, int nIndex, uint value);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true, EntryPoint = "SetWindowLongPtr")]
private static extern uint SetWindowLongPtr(IntPtr hWnd, int nIndex, uint value); private static extern IntPtr SetWindowLong64b(IntPtr hWnd, int nIndex, IntPtr value);
public static uint SetWindowLong(IntPtr hWnd, int nIndex, uint value) public static uint SetWindowLong(IntPtr hWnd, int nIndex, uint value)
{ {
@ -791,7 +791,19 @@ namespace Avalonia.Win32.Interop
} }
else else
{ {
return SetWindowLongPtr(hWnd, nIndex, value); return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32();
}
}
public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle)
{
if (IntPtr.Size == 4)
{
return new IntPtr(SetWindowLong32b(hWnd, nIndex, (uint)handle.ToInt32()));
}
else
{
return SetWindowLong64b(hWnd, nIndex, handle);
} }
} }

9
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -27,11 +27,11 @@ namespace Avalonia
{ {
public static T UseWin32<T>( public static T UseWin32<T>(
this T builder, this T builder,
bool deferredRendering = true) bool deferredRendering = true, bool allowEgl = false)
where T : AppBuilderBase<T>, new() where T : AppBuilderBase<T>, new()
{ {
return builder.UseWindowingSubsystem( return builder.UseWindowingSubsystem(
() => Win32.Win32Platform.Initialize(deferredRendering), () => Win32.Win32Platform.Initialize(deferredRendering, allowEgl),
"Win32"); "Win32");
} }
} }
@ -66,7 +66,7 @@ namespace Avalonia.Win32
Initialize(true); Initialize(true);
} }
public static void Initialize(bool deferredRendering = true) public static void Initialize(bool deferredRendering = true, bool allowEgl = false)
{ {
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardImpl>() .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
@ -80,7 +80,8 @@ namespace Avalonia.Win32
.Bind<IWindowingPlatform>().ToConstant(s_instance) .Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IPlatformIconLoader>().ToConstant(s_instance); .Bind<IPlatformIconLoader>().ToConstant(s_instance);
Win32GlManager.Initialize(); if (allowEgl)
Win32GlManager.Initialize();
UseDeferredRendering = deferredRendering; UseDeferredRendering = deferredRendering;
_uiThread = Thread.CurrentThread; _uiThread = Thread.CurrentThread;

34
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -13,6 +13,7 @@ using Avalonia.Input.Raw;
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.Win32.Input; using Avalonia.Win32.Input;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
@ -42,6 +43,8 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget; private OleDropTarget _dropTarget;
private Size _minSize; private Size _minSize;
private Size _maxSize; private Size _maxSize;
private WindowImpl _parent;
private readonly List<WindowImpl> _disabledBy = new List<WindowImpl>();
#if USE_MANAGED_DRAG #if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag; private readonly ManagedWindowResizeDragHelper _managedDrag;
@ -165,10 +168,10 @@ namespace Avalonia.Win32
private set; private set;
} }
public bool IsEnabled
void UpdateEnabled()
{ {
get { return UnmanagedMethods.IsWindowEnabled(_hwnd); } EnableWindow(_hwnd, _disabledBy.Count == 0);
set { UnmanagedMethods.EnableWindow(_hwnd, value); }
} }
public Size MaxClientSize public Size MaxClientSize
@ -232,8 +235,6 @@ namespace Avalonia.Win32
public void Dispose() public void Dispose()
{ {
_framebuffer?.Dispose();
_framebuffer = null;
if (_hwnd != IntPtr.Zero) if (_hwnd != IntPtr.Zero)
{ {
UnmanagedMethods.DestroyWindow(_hwnd); UnmanagedMethods.DestroyWindow(_hwnd);
@ -248,6 +249,12 @@ namespace Avalonia.Win32
public void Hide() public void Hide()
{ {
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
_parent = null;
}
UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide);
} }
@ -359,6 +366,7 @@ namespace Avalonia.Win32
public virtual void Show() public virtual void Show()
{ {
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero);
ShowWindow(_showWindowState); ShowWindow(_showWindowState);
} }
@ -412,11 +420,13 @@ namespace Avalonia.Win32
} }
} }
public virtual IDisposable ShowDialog() public void ShowDialog(IWindowImpl parent)
{ {
Show(); _parent = (WindowImpl)parent;
_parent._disabledBy.Add(this);
return Disposable.Empty; _parent.UpdateEnabled();
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd);
ShowWindow(_showWindowState);
} }
public void SetCursor(IPlatformHandle cursor) public void SetCursor(IPlatformHandle cursor)
@ -490,6 +500,11 @@ namespace Avalonia.Win32
//Remove root reference to this class, so unmanaged delegate can be collected //Remove root reference to this class, so unmanaged delegate can be collected
s_instances.Remove(this); s_instances.Remove(this);
Closed?.Invoke(); Closed?.Invoke();
if (_parent != null)
{
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
//Free other resources //Free other resources
Dispose(); Dispose();
return IntPtr.Zero; return IntPtr.Zero;
@ -633,7 +648,6 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_PAINT: case UnmanagedMethods.WindowsMessage.WM_PAINT:
UnmanagedMethods.PAINTSTRUCT ps; UnmanagedMethods.PAINTSTRUCT ps;
if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero)
{ {
var f = Scaling; var f = Scaling;

14
tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Controls.UnitTests
{
public class CursorFactoryMock : IStandardCursorFactory
{
public IPlatformHandle GetCursor(StandardCursorType cursorType)
{
return new PlatformHandle(IntPtr.Zero, cursorType.ToString());
}
}
}

284
tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs

@ -0,0 +1,284 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class SharedSizeScopeTests
{
public SharedSizeScopeTests()
{
}
[Fact]
public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
}
[Fact]
public void All_Descendant_Grids_Are_Registered_When_Setting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.Child = scope;
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
}
[Fact]
public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope()
{
var grids = new[] { new Grid(), new Grid(), new Grid() };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
Assert.All(grids, g => Assert.True(g.HasSharedSizeScope()));
root.SetValue(Grid.IsSharedSizeScopeProperty, false);
Assert.All(grids, g => Assert.False(g.HasSharedSizeScope()));
Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty));
}
[Fact]
public void Size_Is_Propagated_Between_Grids()
{
var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))};
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Size_Propagation_Is_Constrained_To_Innermost_Scope()
{
var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) };
var innerScope = new Panel();
innerScope.Children.AddRange(grids);
innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true);
var outerGrid = CreateGrid(("A", new GridLength(0)));
var outerScope = new Panel();
outerScope.Children.AddRange(new[] { outerGrid, innerScope });
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = outerScope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Size_Is_Propagated_Between_Rows_And_Columns()
{
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,30"),
RowDefinitions = new RowDefinitions("*,10")
};
grid.ColumnDefinitions[1].SharedSizeGroup = "A";
grid.RowDefinitions[1].SharedSizeGroup = "A";
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = grid;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(30, grid.RowDefinitions[1].ActualHeight);
}
[Fact]
public void Size_Group_Changes_Are_Tracked()
{
var grids = new[] {
CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())),
CreateGrid(("A", new GridLength(30)), (null, new GridLength())) };
var scope = new Panel();
scope.Children.AddRange(grids);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
grids[0].ColumnDefinitions[0].SharedSizeGroup = "A";
root.Measure(new Size(51, 51));
root.Arrange(new Rect(new Point(), new Point(51, 51)));
Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth);
grids[0].ColumnDefinitions[0].SharedSizeGroup = null;
root.Measure(new Size(52, 52));
root.Arrange(new Rect(new Point(), new Point(52, 52)));
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
}
[Fact]
public void Collection_Changes_Are_Tracked()
{
var grid = CreateGrid(
("A", new GridLength(20)),
("A", new GridLength(30)),
("A", new GridLength(40)),
(null, new GridLength()));
var scope = new Panel();
scope.Children.Add(grid);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth));
grid.ColumnDefinitions.RemoveAt(2);
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth));
grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" });
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth));
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" };
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth));
grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" };
grid.Measure(new Size(200, 200));
grid.Arrange(new Rect(new Point(), new Point(200, 200)));
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth));
}
[Fact]
public void Size_Priorities_Are_Maintained()
{
var sizers = new List<Control>();
var grid = CreateGrid(
("A", new GridLength(20)),
("A", new GridLength(20, GridUnitType.Auto)),
("A", new GridLength(1, GridUnitType.Star)),
("A", new GridLength(1, GridUnitType.Star)),
(null, new GridLength()));
for (int i = 0; i < 3; i++)
sizers.Add(AddSizer(grid, i, 6 + i * 6));
var scope = new Panel();
scope.Children.Add(grid);
var root = new TestRoot();
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Child = scope;
grid.Measure(new Size(100, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// all in group are equal to the first fixed column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth));
grid.ColumnDefinitions[0].SharedSizeGroup = null;
grid.Measure(new Size(100, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// all in group are equal to width (MinWidth) of the sizer in the second column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth));
grid.ColumnDefinitions[1].SharedSizeGroup = null;
grid.Measure(new Size(double.PositiveInfinity, 100));
grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// with no constraint star columns default to the MinWidth of the sizer in the column
Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth));
}
// grid creators
private Grid CreateGrid(params string[] columnGroups)
{
return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width)[] columns)
{
return CreateGrid(columns.Select(c =>
(c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns)
{
return CreateGrid(columns.Select(c =>
(c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray());
}
private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns)
{
var columnDefinitions = new ColumnDefinitions();
columnDefinitions.AddRange(
columns.Select(c => new ColumnDefinition
{
SharedSizeGroup = c.name,
Width = c.width,
MinWidth = c.minWidth,
MaxWidth = c.maxWidth
})
);
var grid = new Grid
{
ColumnDefinitions = columnDefinitions
};
return grid;
}
private Control AddSizer(Grid grid, int column, double size = 30)
{
var ctrl = new Control { MinWidth = size, MinHeight = size };
ctrl.SetValue(Grid.ColumnProperty,column);
grid.Children.Add(ctrl);
return ctrl;
}
}
}

145
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests
TabItem selected; TabItem selected;
var target = new TabControl var target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = new[] Items = new[]
{ {
(selected = new TabItem (selected = new TabItem
@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests
var target = new TabControl var target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = items, Items = items,
}; };
@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests
var target = new TabControl var target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = collection, Items = collection,
}; };
@ -147,7 +147,7 @@ namespace Avalonia.Controls.UnitTests
}, },
Child = new TabControl Child = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = collection, Items = collection,
} }
}; };
@ -172,7 +172,7 @@ namespace Avalonia.Controls.UnitTests
var target = new TabControl var target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
DataContext = "Base", DataContext = "Base",
DataTemplates = DataTemplates =
{ {
@ -182,41 +182,39 @@ namespace Avalonia.Controls.UnitTests
}; };
ApplyTemplate(target); ApplyTemplate(target);
var carousel = (Carousel)target.Pages;
var container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); target.ContentPart.UpdateChild();
container.UpdateChild(); var dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
var dataContext = ((TextBlock)container.Child).DataContext;
Assert.Equal(items[0], dataContext); Assert.Equal(items[0], dataContext);
target.SelectedIndex = 1; target.SelectedIndex = 1;
container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); target.ContentPart.UpdateChild();
container.UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext;
dataContext = ((Button)container.Child).DataContext;
Assert.Equal(items[1], dataContext); Assert.Equal(items[1], dataContext);
target.SelectedIndex = 2; target.SelectedIndex = 2;
dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; target.ContentPart.UpdateChild();
dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
Assert.Equal("Base", dataContext); Assert.Equal("Base", dataContext);
target.SelectedIndex = 3; target.SelectedIndex = 3;
container = (ContentPresenter)carousel.Presenter.Panel.Children[0]; target.ContentPart.UpdateChild();
container.UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
dataContext = ((TextBlock)container.Child).DataContext;
Assert.Equal("Qux", dataContext); Assert.Equal("Qux", dataContext);
target.SelectedIndex = 4; target.SelectedIndex = 4;
dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; target.ContentPart.UpdateChild();
dataContext = target.ContentPart.DataContext;
Assert.Equal("Base", dataContext); Assert.Equal("Base", dataContext);
} }
/// <summary> /// <summary>
/// Non-headered control items should result in TabStripItems with empty content. /// Non-headered control items should result in TabItems with empty header.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If a TabStrip is created with non IHeadered controls as its items, don't try to /// If a TabControl is created with non IHeadered controls as its items, don't try to
/// display the control in the TabStripItem: if the TabStrip is part of a TabControl /// display the control in the header: if the control is part of the header then
/// then *that* will also try to display the control, resulting in dual-parentage /// *that* control would also end up in the content region, resulting in dual-parentage
/// breakage. /// breakage.
/// </remarks> /// </remarks>
[Fact] [Fact]
@ -230,18 +228,20 @@ namespace Avalonia.Controls.UnitTests
var target = new TabControl var target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = items, Items = items,
}; };
ApplyTemplate(target); ApplyTemplate(target);
var result = target.TabStrip.GetLogicalChildren() var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren();
.OfType<TabStripItem>()
.Select(x => x.Content) var result = logicalChildren
.OfType<TabItem>()
.Select(x => x.Header)
.ToList(); .ToList();
Assert.Equal(new object[] { string.Empty, string.Empty }, result); Assert.Equal(new object[] { null, null }, result);
} }
[Fact] [Fact]
@ -249,7 +249,7 @@ namespace Avalonia.Controls.UnitTests
{ {
TabControl target = new TabControl TabControl target = new TabControl
{ {
Template = new FuncControlTemplate<TabControl>(CreateTabControlTemplate), Template = TabControlTemplate(),
Items = new[] Items = new[]
{ {
new TabItem { Header = "Foo" }, new TabItem { Header = "Foo" },
@ -262,70 +262,61 @@ namespace Avalonia.Controls.UnitTests
target.SelectedIndex = 2; target.SelectedIndex = 2;
var carousel = (Carousel)target.Pages; var page = (TabItem)target.SelectedItem;
var page = (TabItem)carousel.SelectedItem;
Assert.Null(page.Content); Assert.Null(page.Content);
} }
private Control CreateTabControlTemplate(TabControl parent) private IControlTemplate TabControlTemplate()
{ {
return new StackPanel return new FuncControlTemplate<TabControl>(parent =>
{
Children =
{
new TabStrip
{
Name = "PART_TabStrip",
Template = new FuncControlTemplate<TabStrip>(CreateTabStripTemplate),
MemberSelector = TabControl.HeaderSelector,
[!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty],
[!!TabStrip.SelectedIndexProperty] = parent[!!TabControl.SelectedIndexProperty]
},
new Carousel
{
Name = "PART_Content",
Template = new FuncControlTemplate<Carousel>(CreateCarouselTemplate),
MemberSelector = TabControl.ContentSelector,
[!Carousel.ItemsProperty] = parent[!TabControl.ItemsProperty],
[!Carousel.SelectedItemProperty] = parent[!TabControl.SelectedItemProperty],
}
}
};
}
private Control CreateTabStripTemplate(TabStrip parent) new StackPanel
{ {
return new ItemsPresenter Children = {
{ new ItemsPresenter
Name = "PART_ItemsPresenter", {
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], Name = "PART_ItemsPresenter",
[!CarouselPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty],
}; [!TabStrip.ItemTemplateProperty] = parent[!TabControl.ItemTemplateProperty],
},
new ContentPresenter
{
Name = "PART_Content",
[!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty],
[!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty],
}
}
});
} }
private Control CreateCarouselTemplate(Carousel control) private IControlTemplate TabItemTemplate()
{ {
return new CarouselPresenter return new FuncControlTemplate<TabItem>(parent =>
{ new ContentPresenter
Name = "PART_ItemsPresenter", {
[!CarouselPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], Name = "PART_ContentPresenter",
[!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty],
[!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty]
[!CarouselPresenter.SelectedIndexProperty] = control[!SelectingItemsControl.SelectedIndexProperty], });
[~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
};
} }
private void ApplyTemplate(TabControl target) private void ApplyTemplate(TabControl target)
{ {
target.ApplyTemplate(); target.ApplyTemplate();
var carousel = (Carousel)target.Pages;
carousel.ApplyTemplate(); target.Presenter.ApplyTemplate();
carousel.Presenter.ApplyTemplate();
var tabStrip = (TabStrip)target.TabStrip; foreach (var tabItem in target.GetLogicalChildren().OfType<TabItem>())
tabStrip.ApplyTemplate(); {
tabStrip.Presenter.ApplyTemplate(); tabItem.Template = TabItemTemplate();
tabItem.ApplyTemplate();
((ContentPresenter)tabItem.Presenter).UpdateChild();
}
target.ContentPart.ApplyTemplate();
} }
private class Item private class Item

7
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -62,9 +62,11 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var parent = new Window();
parent.Show();
var window = new Window(); var window = new Window();
var task = window.ShowDialog(); var task = window.ShowDialog(parent);
Assert.True(window.IsVisible); Assert.True(window.IsVisible);
} }
@ -258,12 +260,13 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var parent = new Mock<IWindowImpl>();
var windowImpl = new Mock<IWindowImpl>(); var windowImpl = new Mock<IWindowImpl>();
windowImpl.SetupProperty(x => x.Closed); windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Scaling).Returns(1);
var target = new Window(windowImpl.Object); var target = new Window(windowImpl.Object);
var task = target.ShowDialog<bool>(); var task = target.ShowDialog<bool>(parent.Object);
windowImpl.Object.Closed(); windowImpl.Object.Closed();

4
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -21,6 +21,7 @@ using Xunit;
using Avalonia.Media; using Avalonia.Media;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.UnitTests;
using Avalonia.UnitTests; using Avalonia.UnitTests;
namespace Avalonia.Layout.UnitTests namespace Avalonia.Layout.UnitTests
@ -98,6 +99,8 @@ namespace Avalonia.Layout.UnitTests
} }
}; };
window.Resources["ScrollBarThickness"] = 10.0;
window.Show(); window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window); window.LayoutManager.ExecuteInitialLayoutPass(window);
@ -197,6 +200,7 @@ namespace Avalonia.Layout.UnitTests
windowImpl.SetupGet(x => x.Scaling).Returns(1); windowImpl.SetupGet(x => x.Scaling).Returns(1);
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryMock())
.Bind<IAssetLoader>().ToConstant(new AssetLoader()) .Bind<IAssetLoader>().ToConstant(new AssetLoader())
.Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object) .Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object)
.Bind<IGlobalStyles>().ToConstant(globalStyles.Object) .Bind<IGlobalStyles>().ToConstant(globalStyles.Object)

100
tests/Avalonia.Markup.UnitTests/Data/BindingTests_RelativeSource.cs

@ -1,9 +1,13 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data; using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -162,5 +166,99 @@ namespace Avalonia.Markup.UnitTests.Data
decorator2.Child = target; decorator2.Child = target;
Assert.Equal("decorator2", target.Text); Assert.Equal("decorator2", target.Text);
} }
[Fact]
public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_BindingPath()
{
TextBlock target;
Decorator decorator1;
Decorator decorator2;
var viewModel = new { Value = "Foo" };
var root1 = new TestRoot
{
Child = decorator1 = new Decorator
{
Name = "decorator1",
Child = target = new TextBlock(),
},
DataContext = viewModel
};
var root2 = new TestRoot
{
Child = decorator2 = new Decorator
{
Name = "decorator2",
},
DataContext = viewModel
};
var binding = new Binding
{
Path = "DataContext.Value",
RelativeSource = new RelativeSource
{
AncestorType = typeof(Decorator),
}
};
target.Bind(TextBox.TextProperty, binding);
Assert.Equal("Foo", target.Text);
decorator1.Child = null;
Assert.Null(target.Text);
decorator2.Child = target;
Assert.Equal("Foo", target.Text);
}
[Fact]
public void Should_Update_When_Detached_And_Attached_To_Visual_Tree_With_ComplexBindingPath()
{
TextBlock target;
Decorator decorator1;
Decorator decorator2;
var vm = new { Foo = new { Value = "Foo" } };
var root1 = new TestRoot
{
Child = decorator1 = new Decorator
{
Name = "decorator1",
Child = target = new TextBlock(),
},
DataContext = vm
};
var root2 = new TestRoot
{
Child = decorator2 = new Decorator
{
Name = "decorator2",
},
DataContext = vm
};
var binding = new Binding
{
Path = "DataContext.Foo.Value",
RelativeSource = new RelativeSource
{
AncestorType = typeof(Decorator),
}
};
target.Bind(TextBox.TextProperty, binding);
Assert.Equal("Foo", target.Text);
decorator1.Child = null;
Assert.Null(target.Text);
decorator2.Child = target;
Assert.Equal("Foo", target.Text);
}
} }
} }

58
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs

@ -1,10 +1,10 @@
using Avalonia.Data; using System;
using Avalonia.Markup.Parsers;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text; using System.Reactive.Subjects;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Parsers;
using Xunit; using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers namespace Avalonia.Markup.UnitTests.Parsers
@ -38,5 +38,55 @@ namespace Avalonia.Markup.UnitTests.Parsers
GC.KeepAlive(data); GC.KeepAlive(data);
} }
[Fact]
public void Should_Update_Value_After_Root_Changes()
{
var root = new { DataContext = new { Value = "Foo" } };
var subject = new Subject<object>();
var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Value");
var values = new List<object>();
obs.Subscribe(v => values.Add(v));
subject.OnNext(root);
subject.OnNext(null);
subject.OnNext(root);
Assert.Equal("Foo", values[0]);
Assert.IsType<BindingNotification>(values[1]);
var bn = values[1] as BindingNotification;
Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value);
Assert.Equal(BindingErrorType.Error, bn.ErrorType);
Assert.Equal(3, values.Count);
Assert.Equal("Foo", values[2]);
}
[Fact]
public void Should_Update_Value_After_Root_Changes_With_ComplexPath()
{
var root = new { DataContext = new { Foo = new { Value = "Foo" } } };
var subject = new Subject<object>();
var obs = ExpressionObserverBuilder.Build(subject, "DataContext.Foo.Value");
var values = new List<object>();
obs.Subscribe(v => values.Add(v));
subject.OnNext(root);
subject.OnNext(null);
subject.OnNext(root);
Assert.Equal("Foo", values[0]);
Assert.IsType<BindingNotification>(values[1]);
var bn = values[1] as BindingNotification;
Assert.Equal(AvaloniaProperty.UnsetValue, bn.Value);
Assert.Equal(BindingErrorType.Error, bn.ErrorType);
Assert.Equal(3, values.Count);
Assert.Equal("Foo", values[2]);
}
} }
} }

4
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs

@ -405,7 +405,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
root.Renderer = new DeferredRenderer(root, null); root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity); root.Measure(Size.Infinity);
root.Arrange(new Rect(container.DesiredSize)); root.Arrange(new Rect(container.DesiredSize));
root.Renderer.Paint(Rect.Empty);
var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First();
Assert.Equal(item1, result); Assert.Equal(item1, result);
@ -421,6 +422,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
container.InvalidateArrange(); container.InvalidateArrange();
container.Arrange(new Rect(container.DesiredSize)); container.Arrange(new Rect(container.DesiredSize));
root.Renderer.Paint(Rect.Empty);
result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); result = root.Renderer.HitTest(new Point(50, 150), root, null).First();
Assert.Equal(item2, result); Assert.Equal(item2, result);

Loading…
Cancel
Save