Skip to content

Commit

Permalink
Merge pull request #254 from h0lg/inline-styles
Browse files Browse the repository at this point in the history
Strongly-typed inline styles
  • Loading branch information
edgarfgp committed Jun 14, 2024
2 parents 8d38c56 + 754d950 commit 5679f10
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 33 deletions.
52 changes: 51 additions & 1 deletion samples/Gallery/Pages/StylesPage.fs
Original file line number Diff line number Diff line change
@@ -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<TextBox>()
.Template()
.OfType<TextBlock>()
(* 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<AutoCompleteBox>()
.Template()
.OfType<TextBox>()
(* 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.) {
Expand All @@ -28,6 +72,12 @@ module StylesPage =

TextBlock("I'm just a text")

AutoCompleteBox([])
.watermark("I'm an AutoCompleteBox styled to have a crimson watermark and accept Return/Enter")
.styles(
[ coloredTextBoxWatermark(Brushes.Crimson)
acceptReturnOnAutoCompleteTextBox() ]
)
})
)
.styles([ "avares://Gallery/Styles/TextStyles.xaml" ])
.styleInclude("avares://Gallery/Styles/TextStyles.xaml")
32 changes: 17 additions & 15 deletions samples/Gallery/Pages/TreeView/EditableTreeView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AutoCompleteBox> 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<AutoCompleteBox> 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.
[<Extension>]
/// 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) =
Expand Down Expand Up @@ -312,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() {
Expand Down
2 changes: 1 addition & 1 deletion samples/RenderDemo/Animations/AnimationsPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ module AnimationsPage =
)
}
)
.styles([ "avares://RenderDemo/Styles/Animations.xaml" ])
.styleInclude([ "avares://RenderDemo/Styles/Animations.xaml" ])
})
.horizontalAlignment(HorizontalAlignment.Center)
.verticalAlignment(VerticalAlignment.Center)
Expand Down
2 changes: 1 addition & 1 deletion samples/RenderDemo/TransitionsPage.fs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ module TransitionsPage =
})
.clipToBounds(false)
)
.styles([ "avares://RenderDemo/Styles/Transitions.xaml" ])
.styleInclude([ "avares://RenderDemo/Styles/Transitions.xaml" ])

})
.horizontalAlignment(HorizontalAlignment.Center)
Expand Down
56 changes: 41 additions & 15 deletions src/Fabulous.Avalonia/Views/_StyledElement.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,16 +19,21 @@ module StyledElement =
let StylesWidget =
Attributes.defineAvaloniaListWidgetCollection "StyledElement_StylesWidget" (fun target -> (target :?> StyledElement).Styles)

let Styles =
Attributes.definePropertyWithGetSet<IStyle seq> "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<string list> "StyledElement_Classes" (fun _ newValueOpt node ->
let target = node.Target :?> StyledElement
Attributes.definePropertyWithGetSet<string seq> "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<string>()
classes |> List.iter coll.Add
target.Classes.AddRange coll)
for an in value do
target.Classes.Add(an))

let ContentType =
Attributes.defineAvaloniaPropertyWithEquality<TextInputContentType> TextInputOptions.ContentTypeProperty
Expand All @@ -52,14 +56,15 @@ module StyledElement =
let IsSensitive =
Attributes.defineAvaloniaPropertyWithEquality TextInputOptions.IsSensitiveProperty

let Styles =
Attributes.defineProperty "StyledElement_Styles" Unchecked.defaultof<string list> (fun target values ->
let styles = (target :?> StyledElement).Styles
let StyleInclude =
Attributes.defineProperty "StyledElement_StyleInclude" Unchecked.defaultof<string list> (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))
target.Styles.Add(style))

let AttachedToLogicalTree =
Attributes.defineEvent<LogicalTreeAttachmentEventArgs> "StyledElement_AttachedToLogicalTree" (fun target ->
Expand Down Expand Up @@ -130,7 +135,6 @@ type StyledElementModifiers =
static member inline contentType(this: WidgetBuilder<'msg, #IFabStyledElement>, value: TextInputContentType) =
this.AddScalar(StyledElement.ContentType.WithValue(value))


/// <summary>Sets the ReturnKeyType property.</summary>
/// <param name="this">Current widget.</param>
/// <param name="value">The ReturnKeyType value.</param>
Expand Down Expand Up @@ -177,9 +181,31 @@ type StyledElementModifiers =
/// <param name="this">Current widget.</param>
/// <param name="value">Application styles to be used for the control.</param>
[<Extension>]
static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string list) =
static member inline styleInclude(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string list) =
this.AddScalar(StyledElement.StyleInclude.WithValue(value))

/// <summary>Sets the application styles.</summary>
/// <param name="this">Current widget.</param>
/// <param name="value">Application styles to be used for the control.</param>
[<Extension>]
static member inline styleInclude(this: WidgetBuilder<'msg, #IFabStyledElement>, value: string) =
StyledElementModifiers.styleInclude(this, [ value ])

/// <summary>Adds inline styles used by the widget and its descendants.</summary>
/// <param name="this">Current widget.</param>
/// <param name="value">Inline styles to be used for the widget and its descendants.</param>
/// <remarks>Note: Fabulous will recreate the Style/Styles during the view diffing as opposed to a single styled element property.</remarks>
[<Extension>]
static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: IStyle list) =
this.AddScalar(StyledElement.Styles.WithValue(value))

/// <summary>Add inline style used by the widget and its descendants.</summary>
/// <param name="this">Current widget.</param>
/// <param name="value">Inline style to be used for the widget and its descendants.</param>
/// <remarks>Note: Fabulous will recreate the Style/Styles during the view diffing as opposed to a single styled element property.</remarks>
static member inline styles(this: WidgetBuilder<'msg, #IFabStyledElement>, value: IStyle) =
StyledElementModifiers.styles(this, [ value ])

/// <summary>Sets the ThemeKey property. The ThemeKey is used to lookup the ControlTheme from the
/// application styles that is applied to the control.</summary>
/// <param name="this">Current widget.</param>
Expand Down

0 comments on commit 5679f10

Please sign in to comment.