Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strongly-typed inline styles #254

Merged
merged 5 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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