From 2367bc084ca0444efe3bbbb49d8c23a9f04c613d Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Thu, 6 Jun 2024 03:23:50 +0200 Subject: [PATCH 1/5] add attribute and extension for adding inline styles and a use case example for it --- samples/Gallery/Pages/StylesPage.fs | 47 +++++++++++++++++++ src/Fabulous.Avalonia/Views/_StyledElement.fs | 13 ++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/samples/Gallery/Pages/StylesPage.fs b/samples/Gallery/Pages/StylesPage.fs index e9e7fd64d..d5f671568 100644 --- a/samples/Gallery/Pages/StylesPage.fs +++ b/samples/Gallery/Pages/StylesPage.fs @@ -1,10 +1,54 @@ namespace Gallery +open Avalonia.Controls +open Avalonia.Media +open Avalonia.Styling open Fabulous.Avalonia open type Fabulous.Avalonia.View module StylesPage = + + let private coloredTextBoxWatermark (color: IBrush) = + (* Create a style that targets the Watermark TextBlock of the TextBox Template, + which is neither accessible in the Logical- nor the VisualTree. *) + let style = + Style( + // see https://docs.avaloniaui.net/docs/reference/styles/style-selector-syntax + _.OfType() + .Template() + .OfType() + (* matches the Name of the Watermark TextBlock in the Avalonia TextBox template; + see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml *) + .Name("PART_Watermark") + ) + + (* Set the Foreground of the nested TextBlock using a StyledProperty + because this is otherwise unsupported by the Avalonia TextBox API. *) + style.Setters.Add(Setter(Avalonia.Controls.TextBlock.ForegroundProperty, box color)) + + style + + let private acceptReturnOnAutoCompleteTextBox () = + (* Create a style that targets the TextBox part of the AutoCompleteBox Template, + which is neither accessible in the Logical- nor the VisualTree. *) + let style = + Style( + // see https://docs.avaloniaui.net/docs/reference/styles/style-selector-syntax + _.OfType() + .Template() + .OfType() + (* matches the Name of the TextBox in the Avalonia AutoCompleteBox template; + see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml *) + .Name("PART_TextBox") + ) + + (* Set the AcceptsReturn of the nested TextBox using a StyledProperty + because this is otherwise unsupported by the Avalonia AutoCompleteBox API. *) + style.Setters.Add(Setter(TextBox.AcceptsReturnProperty, box true)) + + style + let view () = UserControl( (VStack(spacing = 15.) { @@ -28,6 +72,9 @@ module StylesPage = TextBlock("I'm just a text") + AutoCompleteBox([]) + .watermark("I'm an AutoCompleteBox styled to have a crimson watermark and accept Return/Enter") + .inlineStyles(coloredTextBoxWatermark(Brushes.Crimson), acceptReturnOnAutoCompleteTextBox()) }) ) .styles([ "avares://Gallery/Styles/TextStyles.xaml" ]) diff --git a/src/Fabulous.Avalonia/Views/_StyledElement.fs b/src/Fabulous.Avalonia/Views/_StyledElement.fs index 8893258fa..9964ac20b 100644 --- a/src/Fabulous.Avalonia/Views/_StyledElement.fs +++ b/src/Fabulous.Avalonia/Views/_StyledElement.fs @@ -61,6 +61,11 @@ module StyledElement = style.Source <- Uri(value) styles.Add(style)) + /// Allows adding inline styles to a StyledElement. + let InlineStyles = + Attributes.defineProperty "StyledElement_InlineStyles" Unchecked.defaultof (fun target values -> + (target :?> StyledElement).Styles.AddRange values) + let AttachedToLogicalTree = Attributes.defineEvent "StyledElement_AttachedToLogicalTree" (fun target -> (target :?> StyledElement).AttachedToLogicalTree) @@ -130,7 +135,6 @@ type StyledElementModifiers = static member inline contentType(this: WidgetBuilder<'msg, #IFabStyledElement>, value: TextInputContentType) = this.AddScalar(StyledElement.ContentType.WithValue(value)) - /// Sets the ReturnKeyType property. /// Current widget. /// The ReturnKeyType value. @@ -180,6 +184,13 @@ type StyledElementModifiers = static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string list) = this.AddScalar(StyledElement.Styles.WithValue(value)) + /// Adds inline styles used by the widget and its descendants. + /// Current widget. + /// Inline styles to be used for the widget and its descendants. + [] + static member inline inlineStyles(this: WidgetBuilder<'msg, #IFabStyledElement>, [] styles: IStyle[]) = + this.AddScalar(StyledElement.InlineStyles.WithValue(styles)) + /// Sets the ThemeKey property. The ThemeKey is used to lookup the ControlTheme from the /// application styles that is applied to the control. /// Current widget. From 1df26cb192c982bb43cc58bea909178d87dae486 Mon Sep 17 00:00:00 2001 From: Holger Schmidt Date: Thu, 6 Jun 2024 17:11:10 +0200 Subject: [PATCH 2/5] fixed example of focusing auto-complete if empty (i.e. added) --- .../Pages/TreeView/EditableTreeView.fs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/samples/Gallery/Pages/TreeView/EditableTreeView.fs b/samples/Gallery/Pages/TreeView/EditableTreeView.fs index 47f432a3e..0d4829bbb 100644 --- a/samples/Gallery/Pages/TreeView/EditableTreeView.fs +++ b/samples/Gallery/Pages/TreeView/EditableTreeView.fs @@ -13,26 +13,28 @@ open Fabulous open type Fabulous.Avalonia.View module FocusAttributes = - /// Allows setting the Focus on an Avalonia.Input.InputElement + /// Allows setting the Focus on a AutoCompleteBox let Focus = - Attributes.defineBool "Focus" (fun oldValueOpt newValueOpt node -> - let target = node.Target :?> InputElement - - let rec focusAndCleanUp x y = - target.Focus() |> ignore - target.AttachedToVisualTree.RemoveHandler(focusAndCleanUp) // to clean up + let rec focusOnce obj _ = + let autoComplete = unbox obj + autoComplete.Focus(NavigationMethod.Unspecified) |> ignore + autoComplete.TemplateApplied.RemoveHandler(focusOnce) // to clean up + Attributes.defineBool "Focus" (fun _ newValueOpt node -> if newValueOpt.IsSome && newValueOpt.Value then - (* TODO setting the focus on an AutoCompleteBox is broken. - It works for some (probably threading-related) reason if you hit a magic break point here - or in the focusAndCleanUp handler above. *) - Debugger.Break() - target.AttachedToVisualTree.AddHandler(focusAndCleanUp)) + let autoComplete = unbox node.Target + autoComplete.TemplateApplied.RemoveHandler(focusOnce) // to avoid duplicate handlers + + (* Wait to call Focus() on AutoCompleteBox until after TemplateApplied + because of internal Avalonia AutoCompleteBox implementation: + FocusChanged only applies the Focus to the nested TextBox if it is set - which happens in OnApplyTemplate. + See https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs *) + autoComplete.TemplateApplied.AddHandler(focusOnce)) type FocusModifiers = + /// Sets the Focus on an IFabAutoCompleteBox if set is true; otherwise does nothing. [] - /// Sets the Focus on an IFabInputElement if set is true; otherwise does nothing. - static member inline focus(this: WidgetBuilder<'msg, #IFabInputElement>, set: bool) = + static member inline focus(this: WidgetBuilder<'msg, #IFabAutoCompleteBox>, set: bool) = this.AddScalar(FocusAttributes.Focus.WithValue(set)) type EditableNode(name, children) = From 8bad85661d153abb42c728adf35182c930470457 Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Thu, 13 Jun 2024 10:21:40 +0100 Subject: [PATCH 3/5] Use Attributes.definePropertyWithGetSet --- src/Fabulous.Avalonia/Views/_StyledElement.fs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Fabulous.Avalonia/Views/_StyledElement.fs b/src/Fabulous.Avalonia/Views/_StyledElement.fs index 9964ac20b..c29c9aa77 100644 --- a/src/Fabulous.Avalonia/Views/_StyledElement.fs +++ b/src/Fabulous.Avalonia/Views/_StyledElement.fs @@ -20,16 +20,21 @@ module StyledElement = let StylesWidget = Attributes.defineAvaloniaListWidgetCollection "StyledElement_StylesWidget" (fun target -> (target :?> StyledElement).Styles) + let Styles = + Attributes.definePropertyWithGetSet "StyledElement_Styles" (fun target -> (target :?> StyledElement).Styles) (fun target value -> + let target = (target :?> StyledElement) + target.Styles.Clear() + + for an in value do + target.Styles.Add(an)) + let Classes = - Attributes.defineSimpleScalarWithEquality "StyledElement_Classes" (fun _ newValueOpt node -> - let target = node.Target :?> StyledElement + Attributes.definePropertyWithGetSet "StyledElement_Classes" (fun target -> (target :?> StyledElement).Classes) (fun target value -> + let target = (target :?> StyledElement) + target.Classes.Clear() - match newValueOpt with - | ValueNone -> target.Classes.Clear() - | ValueSome classes -> - let coll = AvaloniaList() - classes |> List.iter coll.Add - target.Classes.AddRange coll) + for an in value do + target.Classes.Add(an)) let ContentType = Attributes.defineAvaloniaPropertyWithEquality TextInputOptions.ContentTypeProperty From ddd7f2e0c73337b277f762d049796acab5e12eaf Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Thu, 13 Jun 2024 10:22:11 +0100 Subject: [PATCH 4/5] Rename modifiers for clarity --- src/Fabulous.Avalonia/Views/_StyledElement.fs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Fabulous.Avalonia/Views/_StyledElement.fs b/src/Fabulous.Avalonia/Views/_StyledElement.fs index c29c9aa77..f33b8cc2b 100644 --- a/src/Fabulous.Avalonia/Views/_StyledElement.fs +++ b/src/Fabulous.Avalonia/Views/_StyledElement.fs @@ -3,7 +3,6 @@ namespace Fabulous.Avalonia open System open System.Runtime.CompilerServices open Avalonia -open Avalonia.Collections open Avalonia.Input.TextInput open Avalonia.LogicalTree open Avalonia.Markup.Xaml.Styling @@ -57,19 +56,15 @@ module StyledElement = let IsSensitive = Attributes.defineAvaloniaPropertyWithEquality TextInputOptions.IsSensitiveProperty - let Styles = - Attributes.defineProperty "StyledElement_Styles" Unchecked.defaultof (fun target values -> - let styles = (target :?> StyledElement).Styles + let StyleInclude = + Attributes.defineProperty "StyledElement_StyleInclude" Unchecked.defaultof (fun target values -> + let target = (target :?> StyledElement) + target.Styles.Clear() for value in values do let style = StyleInclude(baseUri = null) style.Source <- Uri(value) - styles.Add(style)) - - /// Allows adding inline styles to a StyledElement. - let InlineStyles = - Attributes.defineProperty "StyledElement_InlineStyles" Unchecked.defaultof (fun target values -> - (target :?> StyledElement).Styles.AddRange values) + target.Styles.Add(style)) let AttachedToLogicalTree = Attributes.defineEvent "StyledElement_AttachedToLogicalTree" (fun target -> @@ -186,15 +181,30 @@ type StyledElementModifiers = /// Current widget. /// Application styles to be used for the control. [] - static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string list) = - this.AddScalar(StyledElement.Styles.WithValue(value)) + static member inline styleInclude(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string list) = + this.AddScalar(StyledElement.StyleInclude.WithValue(value)) + + /// Sets the application styles. + /// Current widget. + /// Application styles to be used for the control. + [] + static member inline styleInclude(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string) = + StyledElementModifiers.styleInclude(this, [ value ]) /// Adds inline styles used by the widget and its descendants. /// Current widget. - /// Inline styles to be used for the widget and its descendants. + /// Inline styles to be used for the widget and its descendants. + /// Note: Fabulous will recreate the Style/Styles during the view diffing as opposed to a single styled element property. [] - static member inline inlineStyles(this: WidgetBuilder<'msg, #IFabStyledElement>, [] styles: IStyle[]) = - this.AddScalar(StyledElement.InlineStyles.WithValue(styles)) + static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: IStyle list) = + this.AddScalar(StyledElement.Styles.WithValue(value)) + + /// Add inline style used by the widget and its descendants. + /// Current widget. + /// Inline style to be used for the widget and its descendants. + /// Note: Fabulous will recreate the Style/Styles during the view diffing as opposed to a single styled element property. + static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: IStyle) = + StyledElementModifiers.styles(this, [ value ]) /// Sets the ThemeKey property. The ThemeKey is used to lookup the ControlTheme from the /// application styles that is applied to the control. From 754d95013e2c49fc0afb344a91c9c3610c1d2b6e Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Thu, 13 Jun 2024 10:22:22 +0100 Subject: [PATCH 5/5] Update samples --- samples/Gallery/Pages/StylesPage.fs | 7 +++++-- samples/Gallery/Pages/TreeView/EditableTreeView.fs | 2 +- samples/RenderDemo/Animations/AnimationsPage.fs | 2 +- samples/RenderDemo/TransitionsPage.fs | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/samples/Gallery/Pages/StylesPage.fs b/samples/Gallery/Pages/StylesPage.fs index d5f671568..bd48cb05f 100644 --- a/samples/Gallery/Pages/StylesPage.fs +++ b/samples/Gallery/Pages/StylesPage.fs @@ -74,7 +74,10 @@ module StylesPage = AutoCompleteBox([]) .watermark("I'm an AutoCompleteBox styled to have a crimson watermark and accept Return/Enter") - .inlineStyles(coloredTextBoxWatermark(Brushes.Crimson), acceptReturnOnAutoCompleteTextBox()) + .styles( + [ coloredTextBoxWatermark(Brushes.Crimson) + acceptReturnOnAutoCompleteTextBox() ] + ) }) ) - .styles([ "avares://Gallery/Styles/TextStyles.xaml" ]) + .styleInclude("avares://Gallery/Styles/TextStyles.xaml") diff --git a/samples/Gallery/Pages/TreeView/EditableTreeView.fs b/samples/Gallery/Pages/TreeView/EditableTreeView.fs index 0d4829bbb..e12152495 100644 --- a/samples/Gallery/Pages/TreeView/EditableTreeView.fs +++ b/samples/Gallery/Pages/TreeView/EditableTreeView.fs @@ -314,7 +314,7 @@ module EditableTreeView = See https://github.com/AvaloniaUI/Avalonia/discussions/13903 and https://github.com/AvaloniaUI/Avalonia/discussions/12397 *) - .styles([ "avares://Gallery/Styles/EditableTreeView.xaml" ]) + .styleInclude([ "avares://Gallery/Styles/EditableTreeView.xaml" ]) (VStack() { HStack() { diff --git a/samples/RenderDemo/Animations/AnimationsPage.fs b/samples/RenderDemo/Animations/AnimationsPage.fs index 2d696105a..ec4414c43 100644 --- a/samples/RenderDemo/Animations/AnimationsPage.fs +++ b/samples/RenderDemo/Animations/AnimationsPage.fs @@ -116,7 +116,7 @@ module AnimationsPage = ) } ) - .styles([ "avares://RenderDemo/Styles/Animations.xaml" ]) + .styleInclude([ "avares://RenderDemo/Styles/Animations.xaml" ]) }) .horizontalAlignment(HorizontalAlignment.Center) .verticalAlignment(VerticalAlignment.Center) diff --git a/samples/RenderDemo/TransitionsPage.fs b/samples/RenderDemo/TransitionsPage.fs index 873e595c2..f6afaa3f1 100644 --- a/samples/RenderDemo/TransitionsPage.fs +++ b/samples/RenderDemo/TransitionsPage.fs @@ -138,7 +138,7 @@ module TransitionsPage = }) .clipToBounds(false) ) - .styles([ "avares://RenderDemo/Styles/Transitions.xaml" ]) + .styleInclude([ "avares://RenderDemo/Styles/Transitions.xaml" ]) }) .horizontalAlignment(HorizontalAlignment.Center)