From 2dce4dfb1e885018b3825c1fc3fce9c367224672 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:47:24 +0900 Subject: [PATCH 01/30] fix classes and styles properties on StyledElement - fix classes property on StyledElement. - move styles property to IStyleHost.fs. --- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 3 +- src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs | 76 +++++++++++ src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 118 +++++++++++++++--- 3 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index 64cdb065..2a394f09 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -1,4 +1,4 @@ - + net6.0 @@ -61,6 +61,7 @@ + diff --git a/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs b/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs new file mode 100644 index 00000000..b033f324 --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs @@ -0,0 +1,76 @@ +namespace Avalonia.FuncUI.DSL + +open Avalonia.Controls +open Avalonia.Controls.Primitives + +[] +module IStyleHost = + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder + open Avalonia.Styling + + + module private Internals = + open System.Collections.Generic + open System.Linq + + let patchStyles (styles: Styles) (newValues: IStyle seq) = + let newValues = newValues |> Seq.toList + + if List.isEmpty newValues then + styles.Clear() + else + styles |> Seq.except newValues |> styles.RemoveAll + + for newIndex, newStyle in List.indexed newValues do + let oldIndex = styles |> Seq.tryFindIndex ((=) newStyle) + + match oldIndex with + | Some oldIndex when oldIndex = newIndex -> () + | Some oldIndex -> styles.Move(oldIndex, newIndex) + | None -> styles.Insert(newIndex, newStyle) + + let compareStyleSeq<'e when 'e :> seq> (a: obj, b: obj) = + match a, b with + | (:? 'e as a), (:? 'e as b) -> Enumerable.SequenceEqual(a, b) + | _ -> a = b + + type IStyleHost with + + /// Use 'classes' instead when possible. + static member styles<'t when 't :> IStyleHost>(value: Styles) : IAttr<'t> = + let getter: ('t -> Styles) = (fun control -> control.Styles) + + let setter: ('t * Styles -> unit) = + (fun (control, value) -> + Internals.patchStyles control.Styles value) + + AttrBuilder<'t> + .CreateProperty( + "Styles", + value, + ValueSome getter, + ValueSome setter, + ValueSome Internals.compareStyleSeq, + fun () -> Styles() + ) + + /// Use 'classes' instead when possible. + static member styles<'t when 't :> IStyleHost>(styles: IStyle list) : IAttr<'t> = + + let getter: ('t -> (IStyle list)) = (fun control -> control.Styles |> Seq.toList) + + let setter: ('t * IStyle list -> unit) = + (fun (control, value) -> Internals.patchStyles control.Styles value) + + let factory = fun () -> [] + + AttrBuilder<'t> + .CreateProperty( + "Styles", + styles, + ValueSome getter, + ValueSome setter, + ValueSome Internals.compareStyleSeq>, + factory + ) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index af9cee8f..95cdebef 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -8,7 +8,83 @@ module StyledElement = open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Styling + + module private Internals = + open System.Linq + + /// pseudoclasse is classe beginning with a ':' character. + let isPseudoClass (s: string) = s.StartsWith(":") + + /// Update `Classes`'s standard classes with new values. + let patchStandardClasses (classes: Classes) (newValues: string seq) = + + let (|PseudoClass|_|) (s: string) = + if isPseudoClass s then + Some PseudoClass + else + None + let newValues = newValues |> Seq.toList + + if List.isEmpty newValues then + classes.Clear() + else + classes + |> Seq.filter (isPseudoClass >> not) + |> Seq.except newValues + |> classes.RemoveAll + + if classes.Count = 0 then + classes.AddRange newValues + else + /// Update Classes to minimize event triggers while taking pseudoclasse into account. + let rec loop insertIndex newValues = + let current = Seq.tryItem insertIndex classes + + match current, newValues with + | _, [] -> + // If there are no more values, update finished. + () + | None, _ -> + // If there are no more classes in the current classes, add the new values. + classes.AddRange(newValues) + | Some PseudoClass, _ -> + // If the current class is a pseudo class, skip it. + loop (insertIndex + 1) newValues + | Some current, newClass :: rest when current = newClass -> + // If the current class is the same as the new class, skip it. + loop (insertIndex + 1) rest + | Some _, newClass :: rest -> + // Search for the new class in the current classes. + let oldIndex = classes |> Seq.tryFindIndex ((=) newClass) + + match oldIndex with + + | Some oldIndex when oldIndex = insertIndex -> + // If oldIndex is the same as insertIndex, do nothing. + () + | Some oldIndex -> + // If oldIndex is different from insertIndex, move the class to the right position. + classes.Move(oldIndex, insertIndex) + | None -> + // If the class is not in the current classes, insert it. + classes.Insert(insertIndex, newClass) + + // Continue with the next class. + loop (insertIndex + 1) rest + + loop 0 newValues + + /// Compare two sequences of standard classes. + let compareClasses<'e when 'e :> seq > (a: obj, b: obj) : bool = + let setup (o: obj) = + o :?> 'e |> Seq.filter (isPseudoClass >> not) + + let a = setup a + let b = setup b + + Enumerable.SequenceEqual(a, b) + type StyledElement with static member dataContext<'t when 't :> StyledElement>(dataContext: obj) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(StyledElement.DataContextProperty, dataContext, ValueNone) @@ -23,26 +99,30 @@ module StyledElement = AttrBuilder<'t>.CreateProperty(StyledElement.ThemeProperty, theme, ValueNone) static member classes<'t when 't :> StyledElement>(value: Classes) : IAttr<'t> = - let getter : ('t -> Classes) = (fun control -> control.Classes) - let setter : ('t * Classes -> unit) = (fun (control, value) -> - control.Classes.Clear() - control.Classes.AddRange(value)) - - AttrBuilder<'t>.CreateProperty("Classes", value, ValueSome getter, ValueSome setter, ValueNone, fun () -> Classes()) - + let getter: ('t -> Classes) = (fun control -> control.Classes) + + let setter: ('t * Classes -> unit) = + (fun (control, value) -> + Internals.patchStandardClasses control.Classes value) + + let compare = Internals.compareClasses + + let factory = (fun () -> Classes()) + + AttrBuilder<'t>.CreateProperty("Classes", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + static member classes<'t when 't :> StyledElement>(classes: string list) : IAttr<'t> = - classes |> Classes |> StyledElement.classes - - /// Use 'classes' instead when possible. - static member styles<'t when 't :> StyledElement>(value: Styles) : IAttr<'t> = - let getter : ('t -> Styles) = (fun control -> control.Styles) - let setter : ('t * Styles -> unit) = - (fun (control, value) -> - control.Styles.Clear() - control.Styles.AddRange(value)) - - AttrBuilder<'t>.CreateProperty("Styles", value, ValueSome getter, ValueSome setter, ValueNone, fun () -> Styles()) - + let getter: ('t -> (string list)) =(fun control -> Seq.toList control.Classes) + + let setter: ('t * string list -> unit) = + (fun (control, values) -> Internals.patchStandardClasses control.Classes values) + + let compare = Internals.compareClasses + + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty("Classes", classes, ValueSome getter, ValueSome setter, ValueSome compare, factory) + static member resources<'t when 't :> StyledElement>(value: IResourceDictionary) : IAttr<'t> = let getter : ('t -> IResourceDictionary) = (fun control -> control.Resources) let setter : ('t * IResourceDictionary -> unit) = (fun (control, value) -> control.Resources <- value) From 883539d160fb79f379286bdb5fecac0f9a7bf8f4 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:16:32 +0900 Subject: [PATCH 02/30] control catalog: add styles demo back - add styles demo back - update styles.xaml for FluentTheme --- .../Views/MainView.fs | 9 ++++----- .../Views/Tabs/Styles.xaml | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/MainView.fs b/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/MainView.fs index 6f9f3dda..ff6da5c7 100644 --- a/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/MainView.fs +++ b/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/MainView.fs @@ -76,11 +76,10 @@ module MainView = TabItem.header "SplitView Demo" TabItem.content (ViewBuilder.Create([])) ] - // ToDo: return it back when styles will be worked - //TabItem.create [ - // TabItem.header "Styles Demo" - // TabItem.content (ViewBuilder.Create([])) - //] + TabItem.create [ + TabItem.header "Styles Demo" + TabItem.content (ViewBuilder.Create([])) + ] TabItem.create [ TabItem.header "TextBox Demo" TabItem.content (ViewBuilder.Create([])) diff --git a/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/Tabs/Styles.xaml b/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/Tabs/Styles.xaml index 6bb5ecb5..fb8fb126 100644 --- a/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/Tabs/Styles.xaml +++ b/src/Avalonia.FuncUI.ControlCatalog/Avalonia.FuncUI.ControlCatalog/Views/Tabs/Styles.xaml @@ -1,7 +1,16 @@ - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sys="using:System"> + + + + 10 + 12 + 16 + + + @@ -22,9 +31,9 @@ From de6c577e386797416d4dde61b346d3fe971c6406 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:52:20 +0900 Subject: [PATCH 03/30] fix IStyleHost.styles - compare list of IStyle correctly - setter should also update Resources --- src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs | 60 +++++++++++----------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs b/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs index b033f324..01627a6e 100644 --- a/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs +++ b/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs @@ -1,8 +1,5 @@ namespace Avalonia.FuncUI.DSL -open Avalonia.Controls -open Avalonia.Controls.Primitives - [] module IStyleHost = open Avalonia.FuncUI.Types @@ -11,8 +8,6 @@ module IStyleHost = module private Internals = - open System.Collections.Generic - open System.Linq let patchStyles (styles: Styles) (newValues: IStyle seq) = let newValues = newValues |> Seq.toList @@ -30,11 +25,6 @@ module IStyleHost = | Some oldIndex -> styles.Move(oldIndex, newIndex) | None -> styles.Insert(newIndex, newStyle) - let compareStyleSeq<'e when 'e :> seq> (a: obj, b: obj) = - match a, b with - | (:? 'e as a), (:? 'e as b) -> Enumerable.SequenceEqual(a, b) - | _ -> a = b - type IStyleHost with /// Use 'classes' instead when possible. @@ -43,34 +33,42 @@ module IStyleHost = let setter: ('t * Styles -> unit) = (fun (control, value) -> - Internals.patchStyles control.Styles value) - - AttrBuilder<'t> - .CreateProperty( - "Styles", - value, - ValueSome getter, - ValueSome setter, - ValueSome Internals.compareStyleSeq, - fun () -> Styles() - ) + Internals.patchStyles control.Styles value + control.Styles.Resources <- value.Resources) + + let compare: (obj * obj -> bool) = + fun (a, b) -> + match a, b with + | (:? Styles as a), (:? Styles as b) -> + System.Linq.Enumerable.SequenceEqual(a, b) + && System.Linq.Enumerable.SequenceEqual(a.Resources, b.Resources) + && System.Linq.Enumerable.SequenceEqual( + a.Resources.MergedDictionaries, + b.Resources.MergedDictionaries + ) + && System.Linq.Enumerable.SequenceEqual( + a.Resources.ThemeDictionaries, + b.Resources.ThemeDictionaries + ) + | _ -> a = b + + let factory = fun () -> Styles() + + AttrBuilder<'t>.CreateProperty("Styles", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) /// Use 'classes' instead when possible. static member styles<'t when 't :> IStyleHost>(styles: IStyle list) : IAttr<'t> = - let getter: ('t -> (IStyle list)) = (fun control -> control.Styles |> Seq.toList) let setter: ('t * IStyle list -> unit) = (fun (control, value) -> Internals.patchStyles control.Styles value) + let compare: (obj * obj -> bool) = + fun (a, b) -> + match a, b with + | (:? list as a), (:? list as b) -> System.Linq.Enumerable.SequenceEqual(a, b) + | _ -> a = b + let factory = fun () -> [] - AttrBuilder<'t> - .CreateProperty( - "Styles", - styles, - ValueSome getter, - ValueSome setter, - ValueSome Internals.compareStyleSeq>, - factory - ) + AttrBuilder<'t>.CreateProperty("Styles", styles, ValueSome getter, ValueSome setter, ValueSome compare, factory) From b61fb25145729d97b53366363212a4e3e98a91c5 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:51:02 +0900 Subject: [PATCH 04/30] add tests for `IStyleHost.styles` and `Control.classes` properties. --- .../Avalonia.FuncUI.UnitTests.fsproj | 4 +- .../DSL/Base/IStyleHostTests.fs | 109 ++++++++++++++++++ .../DSL/Base/StyledElementTests.fs | 52 +++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs create mode 100644 src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs diff --git a/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj b/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj index 5f453959..e4e85b56 100644 --- a/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj +++ b/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj @@ -1,4 +1,4 @@ - + net6.0 @@ -16,6 +16,8 @@ + + diff --git a/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs b/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs new file mode 100644 index 00000000..84752426 --- /dev/null +++ b/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs @@ -0,0 +1,109 @@ +namespace Avalonia.FuncUI.UnitTests.DSL + +open Avalonia.Controls +open Avalonia.Styling +open global.Xunit + +module IStyleHostTests = + open Avalonia.FuncUI.VirtualDom + open Avalonia.FuncUI.DSL + open Avalonia.FuncUI.Types + + let twoAttrs<'x, 't> (attr: 'x -> IAttr<'t>) a b = + [ attr a :> IAttr ], [ attr b :> IAttr ] + + + let initStyle () = + let s = Style(fun x -> x.Is()) + s.Setters.Add(Setter(Control.TagProperty, "foo")) + s :> IStyle + + [] + let ``styles equality with style list has same style instance`` () = + let style = initStyle () + + let styleList () = [ style ] + + let styles1 = styleList () + let styles2 = styleList () + + let styleList = + (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes + + Assert.Empty styleList + + + [] + let ``styles equality with style list has different style instance`` () = + + let style1 = initStyle () + let style2 = initStyle () + + let styleList = + ([ style1 ], [ style2 ]) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes + + match Assert.Single styleList with + | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } + Value = Some(:? list as [ value ]) } -> + Assert.Equal("Styles", propName) + Assert.NotEqual(style1, value) + Assert.Equal(style2, value) + + | _ -> Assert.Fail $"Not expected delta\n{styleList}" + + [] + let ``styles equality with Styles property has same instance`` () = + let style = initStyle () + + let styles = Styles() + styles.Add style + + let styles1 = styles + let styles2 = styles + + let styleList = + (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes + + Assert.Empty styleList + + [] + let ``styles equality with Styles property has different Styles instance has same style instance`` () = + let style = initStyle () + + let styles1 = Styles() + styles1.Add style + styles1.Resources.Add("key", "value") + + let styles2 = Styles() + styles2.Add style + styles2.Resources.Add("key", "value") + + let styleList = + (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes + + Assert.Empty styleList + + [] + let ``styles equality with Styles property has different Styles instance has different style instance`` () = + let style1 = initStyle () + let style2 = initStyle () + + let styles1 = Styles() + styles1.Add style1 + styles1.Resources.Add("key", "value") + + let styles2 = Styles() + styles2.Add style2 + styles2.Resources.Add("key", "value") + + let styleList = + (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes + + match Assert.Single styleList with + | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } + Value = Some(:? Styles as value) } -> + Assert.Equal("Styles", propName) + Assert.NotEqual(styles1, value) + Assert.Equal(styles2, value) + + | _ -> Assert.Fail $"Not expected delta\n{styleList}" diff --git a/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs b/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs new file mode 100644 index 00000000..4121975f --- /dev/null +++ b/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs @@ -0,0 +1,52 @@ +namespace Avalonia.FuncUI.UnitTests.DSL + +open Avalonia.Controls +open global.Xunit + +module StyledElementTests = + open Avalonia.FuncUI.VirtualDom + open Avalonia.FuncUI.DSL + open Avalonia.FuncUI.Types + + let twoAttrs<'x, 't> (attr: 'x -> IAttr<'t>) a b = + [ attr a :> IAttr ], [ attr b :> IAttr ] + + [] + let ``classes equality with string list`` () = + let valueList() = [ "class1"; "class2" ] + + let classes1 = valueList() + let classes2 = valueList() + + let stringList = + (classes1, classes2) + ||> twoAttrs Control.classes + |> Differ.diffAttributes + + Assert.Empty stringList + + [] + let ``classes equality with same classes instance`` () = + let classes = Classes() + classes.Add "class1" + classes.Add "class2" + + let sameClassesInstance = + (classes, classes) ||> twoAttrs Control.classes |> Differ.diffAttributes + + Assert.Empty sameClassesInstance + + [] + let ``classes equality with different classes instance`` () = + let classes1 = Classes() + classes1.Add "class1" + classes1.Add "class2" + + let classes2 = Classes() + classes2.Add "class1" + classes2.Add "class2" + + let differentClassesInstance = + (classes1, classes2) ||> twoAttrs Control.classes |> Differ.diffAttributes + + Assert.Empty differentClassesInstance From b787bb23c302c780b826c3c9d22984f259932881 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:19:58 +0900 Subject: [PATCH 05/30] add dataTemplates property. --- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 1 + .../DSL/Base/IDataTemplateHost.fs | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index 2a394f09..3a4e52a8 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -64,6 +64,7 @@ + diff --git a/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs b/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs new file mode 100644 index 00000000..78e19ccb --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs @@ -0,0 +1,49 @@ +namespace Avalonia.FuncUI.DSL + +[] +module IDataTemplateHost = + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder + open Avalonia.Controls.Templates + + module private Internals = + let patchDataTemplates(dataTemplates: DataTemplates) (templates: IDataTemplate seq) = + + if Seq.isEmpty templates then + dataTemplates.Clear() + else + templates + |> Seq.except dataTemplates + |> dataTemplates.RemoveAll + + for newIndex, template in Seq.indexed templates do + let oldIndex = dataTemplates |> Seq.tryFindIndex template.Equals + + match oldIndex with + | Some oldIndex when oldIndex = newIndex -> () + | Some oldIndex -> dataTemplates.Move(oldIndex, newIndex) + | None -> dataTemplates.Insert(newIndex, template) + + type IDataTemplateHost with + static member dataTemplates<'t when 't :> IDataTemplateHost>(templates: DataTemplates) : IAttr<'t> = + let getter: 't -> DataTemplates = (fun control -> control.DataTemplates) + + let setter: ('t * DataTemplates -> unit) = (fun (control, value) -> Internals.patchDataTemplates control.DataTemplates value) + + let compare: obj * obj -> bool = (fun (a, b) -> + let a = a :?> DataTemplates + let b = b :?> DataTemplates + System.Linq.Enumerable.SequenceEqual(a, b)) + + let factory = fun () -> DataTemplates() + + AttrBuilder<'t>.CreateProperty("DataTemplates", templates, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + static member dataTemplates<'t when 't :> IDataTemplateHost>(templates: IDataTemplate list) : IAttr<'t> = + let getter: 't -> IDataTemplate list = (fun control -> control.DataTemplates |> Seq.toList) + + let setter: ('t * IDataTemplate list -> unit) = (fun (control, value) -> Internals.patchDataTemplates control.DataTemplates value) + + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty("DataTemplates", templates, ValueSome getter, ValueSome setter, ValueNone, factory) From a5942b8e11d2dfee83760c7fa9559f599064edee Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:51:11 +0900 Subject: [PATCH 06/30] add onPropertyChanged event. --- src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs b/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs index fef84e17..789838bb 100644 --- a/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs +++ b/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs @@ -3,9 +3,12 @@ namespace Avalonia.FuncUI.DSL open Avalonia open Avalonia.FuncUI open Avalonia.FuncUI.Types +open System.Threading [] module AvaloniaObject = + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder type AvaloniaObject with @@ -31,6 +34,16 @@ module AvaloniaObject = InitFunction.Function = (fun (control: obj) -> func (control :?> 't)) } + static member onPropertyChanged<'t when 't :> AvaloniaObject>(func: AvaloniaPropertyChangedEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = + let factory: AvaloniaObject * (AvaloniaPropertyChangedEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.PropertyChanged.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription("PropertyChanged", factory, func, ?subPatchOptions = subPatchOptions) + member this.Bind(prop: DirectPropertyBase<'value>, readable: #IReadable<'value>) : unit = let _ = this.Bind(property = prop, source = readable.ImmediateObservable) () From d33a7bd0b15296a73a02888cebca2b67a1c1c841 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 19:44:59 +0900 Subject: [PATCH 07/30] add Net Event Attr functions. --- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index 95cdebef..87178115 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -8,8 +8,10 @@ module StyledElement = open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Styling + open Avalonia.LogicalTree + open System.Threading - module private Internals = + module internal ClassesInternals = open System.Linq /// pseudoclasse is classe beginning with a ':' character. @@ -86,6 +88,67 @@ module StyledElement = Enumerable.SequenceEqual(a, b) type StyledElement with + static member onAttachedToLogicalTree<'t when 't :> StyledElement>(func:LogicalTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * (LogicalTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.AttachedToLogicalTree.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription("AttachedToLogicalTree", factory, func, ?subPatchOptions = subPatchOptions) + + static member onDetachedFromLogicalTree<'t when 't :> StyledElement>(func:LogicalTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * (LogicalTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.DetachedFromLogicalTree.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription("DetachedFromLogicalTree", factory, func, ?subPatchOptions = subPatchOptions) + + static member onDataContextChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = + control.DataContextChanged.Subscribe(fun _ -> func control) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription<'t>("DataContextChanged", factory, func, ?subPatchOptions = subPatchOptions) + + static member onInitialized<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.Initialized.Subscribe(fun _ -> func control) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription<'t>("Initialized", factory, func, ?subPatchOptions = subPatchOptions) + + static member onResourcesChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.ResourcesChanged.Subscribe(fun _ -> func control) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription<'t>("ResourcesChanged", factory, func, ?subPatchOptions = subPatchOptions) + + static member onActualThemeVariantChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.ActualThemeVariantChanged.Subscribe(fun _ -> func control) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription<'t>("ActualThemeVariantChanged", factory, func, ?subPatchOptions = subPatchOptions) + static member dataContext<'t when 't :> StyledElement>(dataContext: obj) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(StyledElement.DataContextProperty, dataContext, ValueNone) @@ -103,21 +166,21 @@ module StyledElement = let setter: ('t * Classes -> unit) = (fun (control, value) -> - Internals.patchStandardClasses control.Classes value) + ClassesInternals.patchStandardClasses control.Classes value) - let compare = Internals.compareClasses + let compare = ClassesInternals.compareClasses let factory = (fun () -> Classes()) AttrBuilder<'t>.CreateProperty("Classes", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) static member classes<'t when 't :> StyledElement>(classes: string list) : IAttr<'t> = - let getter: ('t -> (string list)) =(fun control -> Seq.toList control.Classes) + let getter: ('t -> (string list)) = (fun control -> Seq.toList control.Classes) let setter: ('t * string list -> unit) = - (fun (control, values) -> Internals.patchStandardClasses control.Classes values) + (fun (control, values) -> ClassesInternals.patchStandardClasses control.Classes values) - let compare = Internals.compareClasses + let compare = ClassesInternals.compareClasses let factory = fun () -> [] From c22962392a2d1899b8a300dea9cf2b1ae5d2ff7b Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 19:52:33 +0900 Subject: [PATCH 08/30] add Visual DSL functions. --- src/Avalonia.FuncUI/DSL/Base/Visual.fs | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/Visual.fs b/src/Avalonia.FuncUI/DSL/Base/Visual.fs index 91beb089..4f1ece2e 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Visual.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Visual.fs @@ -7,9 +7,29 @@ module Visual = open Avalonia.FuncUI open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder + open System.Threading type Visual with - + static member onAttachedToVisualTree<'t when 't :> Visual>(func: VisualTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let factory: AvaloniaObject * (VisualTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> Visual + let disposable = control.AttachedToVisualTree.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription("AttachedToVisualTree", factory, func, ?subPatchOptions = subPatchOptions) + + static member onDetachedFromVisualTree<'t when 't :> Visual>(func: VisualTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let factory:AvaloniaObject * (VisualTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> Visual + let disposable = control.DetachedFromVisualTree.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription("DetachedFromVisualTree", factory, func, ?subPatchOptions = subPatchOptions) + static member clipToBounds<'t when 't :> Visual>(value: bool) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.ClipToBoundsProperty, value, ValueNone) @@ -25,11 +45,17 @@ module Visual = static member opacityMask<'t when 't :> Visual>(value: IBrush) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.OpacityMaskProperty, value, ValueNone) + static member effect<'t when 't :> Visual>(value: IEffect) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Visual.EffectProperty, value, ValueNone) + static member renderTransform<'t when 't :> Visual>(transform: ITransform) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.RenderTransformProperty, transform, ValueSome EqualityComparers.compareTransforms) static member renderTransformOrigin<'t when 't :> Visual>(origin: RelativePoint) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.RenderTransformOriginProperty, origin, ValueNone) - + + static member flowDirection<'t when 't :> Visual>(direction: FlowDirection) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Visual.FlowDirectionProperty, direction, ValueNone) + static member zIndex<'t when 't :> Visual>(index: int) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.ZIndexProperty, index, ValueNone) \ No newline at end of file From 19b68aa34eb07796bddddaf71487630fe27dd24b Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 20:48:24 +0900 Subject: [PATCH 09/30] use `nameof` expression --- src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs | 3 ++- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 18 ++++++++++++------ src/Avalonia.FuncUI/DSL/Base/Visual.fs | 10 ++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs b/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs index 789838bb..8783dadc 100644 --- a/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs +++ b/src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs @@ -35,6 +35,7 @@ module AvaloniaObject = } static member onPropertyChanged<'t when 't :> AvaloniaObject>(func: AvaloniaPropertyChangedEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.PropertyChanged let factory: AvaloniaObject * (AvaloniaPropertyChangedEventArgs -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -42,7 +43,7 @@ module AvaloniaObject = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription("PropertyChanged", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) member this.Bind(prop: DirectPropertyBase<'value>, readable: #IReadable<'value>) : unit = let _ = this.Bind(property = prop, source = readable.ImmediateObservable) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index 87178115..fdc8ed6e 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -89,6 +89,7 @@ module StyledElement = type StyledElement with static member onAttachedToLogicalTree<'t when 't :> StyledElement>(func:LogicalTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.AttachedToLogicalTree let factory: AvaloniaObject * (LogicalTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -96,9 +97,10 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription("AttachedToLogicalTree", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) static member onDetachedFromLogicalTree<'t when 't :> StyledElement>(func:LogicalTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.DetachedFromLogicalTree let factory: AvaloniaObject * (LogicalTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -106,9 +108,10 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription("DetachedFromLogicalTree", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) static member onDataContextChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.DataContextChanged let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -117,9 +120,10 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription<'t>("DataContextChanged", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) static member onInitialized<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.Initialized let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -127,9 +131,10 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription<'t>("Initialized", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) static member onResourcesChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.ResourcesChanged let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -137,9 +142,10 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription<'t>("ResourcesChanged", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) static member onActualThemeVariantChanged<'t when 't :> StyledElement>(func: 't -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.ActualThemeVariantChanged let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't @@ -147,7 +153,7 @@ module StyledElement = token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription<'t>("ActualThemeVariantChanged", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) static member dataContext<'t when 't :> StyledElement>(dataContext: obj) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(StyledElement.DataContextProperty, dataContext, ValueNone) diff --git a/src/Avalonia.FuncUI/DSL/Base/Visual.fs b/src/Avalonia.FuncUI/DSL/Base/Visual.fs index 4f1ece2e..aa2f4220 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Visual.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Visual.fs @@ -11,24 +11,26 @@ module Visual = type Visual with static member onAttachedToVisualTree<'t when 't :> Visual>(func: VisualTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.AttachedToVisualTree let factory: AvaloniaObject * (VisualTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = (fun (control, func, token) -> - let control = control :?> Visual + let control = control :?> 't let disposable = control.AttachedToVisualTree.Subscribe(func) token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription("AttachedToVisualTree", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) static member onDetachedFromVisualTree<'t when 't :> Visual>(func: VisualTreeAttachmentEventArgs -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.DetachedFromVisualTree let factory:AvaloniaObject * (VisualTreeAttachmentEventArgs -> unit) * CancellationToken -> unit = (fun (control, func, token) -> - let control = control :?> Visual + let control = control :?> 't let disposable = control.DetachedFromVisualTree.Subscribe(func) token.Register(fun () -> disposable.Dispose()) |> ignore) - AttrBuilder<'t>.CreateSubscription("DetachedFromVisualTree", factory, func, ?subPatchOptions = subPatchOptions) + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) static member clipToBounds<'t when 't :> Visual>(value: bool) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Visual.ClipToBoundsProperty, value, ValueNone) From f42c4ebcb1eb5cd368e569c3d516f1f19a5be22d Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:04:46 +0900 Subject: [PATCH 10/30] add Layoutable DSL functions. --- src/Avalonia.FuncUI/DSL/Base/Layoutable.fs | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs b/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs index d826076f..90c14a18 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs @@ -2,12 +2,35 @@ namespace Avalonia.FuncUI.DSL [] module Layoutable = + open System.Threading open Avalonia open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Layout type Layoutable with + static member onEffectiveViewportChanged<'t when 't :> Layoutable>(func: EffectiveViewportChangedEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.EffectiveViewportChanged + let factory: AvaloniaObject * (EffectiveViewportChangedEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.EffectiveViewportChanged.Subscribe(func) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onLayoutUpdated<'t when 't :> Layoutable>(func: 't -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.LayoutUpdated + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let disposable = control.LayoutUpdated.Subscribe(fun _ -> func control) + + token.Register(fun () -> disposable.Dispose()) |> ignore) + + AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) + static member width<'t when 't :> Layoutable>(value: double) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Layoutable.WidthProperty, value, ValueNone) From 729ba9f9ba8a26448d0ac3a39bc767c953df59c4 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:54:28 +0900 Subject: [PATCH 11/30] add InputElement DSL functions. --- src/Avalonia.FuncUI/DSL/Base/InputElement.fs | 54 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/InputElement.fs b/src/Avalonia.FuncUI/DSL/Base/InputElement.fs index 4e0eb8f4..26018433 100644 --- a/src/Avalonia.FuncUI/DSL/Base/InputElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/InputElement.fs @@ -6,6 +6,33 @@ module InputElement = open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Input + open Avalonia.Input.TextInput + + module private Internals = + open System.Linq + open System.Collections.Generic + let patchKeyBindings (keyBindings: List) (newValues: KeyBinding list) = + if List.isEmpty newValues then + keyBindings.Clear() + else + Seq.toList keyBindings + |> List.except newValues + |> List.iter (keyBindings.Remove >> ignore) + + for newIndex, binding in List.indexed newValues do + let oldIndex = keyBindings |> Seq.tryFindIndex ((=) binding) + + match oldIndex with + | Some oldIndex when oldIndex = newIndex -> () + | Some oldIndex -> + keyBindings.RemoveAt(oldIndex) + keyBindings.Insert(newIndex, binding) + | None -> keyBindings.Insert(newIndex, binding) + + let compareKeyBindings (a:obj, b:obj) = + let a = a :?> list + let b = b :?> list + Enumerable.SequenceEqual(a, b) type InputElement with static member focusable<'t when 't :> InputElement>(value: bool) : IAttr<'t> = @@ -19,7 +46,22 @@ module InputElement = static member isHitTestVisible<'t when 't :> InputElement>(value: bool) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(InputElement.IsHitTestVisibleProperty, value, ValueNone) - + + static member isTabStop<'t when 't :> InputElement>(value: bool) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(InputElement.IsTabStopProperty, value, ValueNone) + + static member tabIndex<'t when 't :> InputElement>(value: int) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(InputElement.TabIndexProperty, value, ValueNone) + + static member keyBindings<'t when 't :> InputElement>(value: KeyBinding list) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.KeyBindings + let getter: 't -> KeyBinding list = (fun control -> Seq.toList control.KeyBindings) + let setter: 't * KeyBinding list -> unit = (fun (control, value) -> Internals.patchKeyBindings control.KeyBindings value) + let compare: obj * obj -> bool = Internals.compareKeyBindings + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty(name, value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + static member onGotFocus<'t when 't :> InputElement>(func: GotFocusEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(InputElement.GotFocusEvent, func, ?subPatchOptions = subPatchOptions) @@ -34,7 +76,10 @@ module InputElement = static member onTextInput<'t when 't :> InputElement>(func: TextInputEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(InputElement.TextInputEvent, func, ?subPatchOptions = subPatchOptions) - + + static member onTextInputMethodClientRequested<'t when 't :> InputElement>(func: TextInputMethodClientRequestedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(InputElement.TextInputMethodClientRequestedEvent, func, ?subPatchOptions = subPatchOptions) + static member onPointerEntered<'t when 't :> InputElement>(func: PointerEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(InputElement.PointerEnteredEvent, func, ?subPatchOptions = subPatchOptions) @@ -58,6 +103,9 @@ module InputElement = static member onTapped<'t when 't :> InputElement>(func: TappedEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(InputElement.TappedEvent, func, ?subPatchOptions = subPatchOptions) - + + static member onHolding<'t when 't :> InputElement>(func: HoldingRoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(InputElement.HoldingEvent, func, ?subPatchOptions = subPatchOptions) + static member onDoubleTapped<'t when 't :> InputElement>(func: TappedEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(InputElement.DoubleTappedEvent, func, ?subPatchOptions = subPatchOptions) \ No newline at end of file From 35ee9f07e686fa6e07bef2f8ad5ace78f8be705c Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:59:44 +0900 Subject: [PATCH 12/30] add Control DSL functions. --- src/Avalonia.FuncUI/DSL/Base/Control.fs | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Avalonia.FuncUI/DSL/Base/Control.fs b/src/Avalonia.FuncUI/DSL/Base/Control.fs index dd526cc2..a598f2dd 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Control.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Control.fs @@ -3,6 +3,8 @@ namespace Avalonia.FuncUI.DSL [] module Control = open Avalonia.Controls + open Avalonia.Controls.Primitives + open Avalonia.Interactivity open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder @@ -32,3 +34,28 @@ module Control = static member contextMenu<'t when 't :> Control>(menu: ContextMenu) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Control.ContextMenuProperty, menu, ValueNone) + static member contextFlyout<'t when 't :> Control>(flyoutView: IView option) : IAttr<'t> = + let view = + match flyoutView with + | Some view -> Some (view :> IView) + | None -> None + + AttrBuilder<'t>.CreateContentSingle(Control.ContextFlyoutProperty, view) + + static member contextFlyout<'t when 't :> Control>(flyoutView: IView) : IAttr<'t> = + AttrBuilder<'t>.CreateContentSingle(Control.ContextFlyoutProperty, Some (flyoutView :> IView)) + + static member contextFlyout<'t when 't :> Control>(flyout: FlyoutBase) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Control.ContextFlyoutProperty, flyout, ValueNone) + + static member onContextRequested<'t when 't :> Control>(func: ContextRequestedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Control.ContextRequestedEvent, func, ?subPatchOptions = subPatchOptions) + + static member onLoaded<'t when 't :> Control>(func: RoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Control.LoadedEvent, func, ?subPatchOptions = subPatchOptions) + + static member onUnloaded<'t when 't :> Control>(func: RoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Control.UnloadedEvent, func, ?subPatchOptions = subPatchOptions) + + static member onSizeChanged<'t when 't :> Control>(func: SizeChangedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Control.SizeChangedEvent, func, ?subPatchOptions = subPatchOptions) From 023dde82fe1828cebea5012789b1dfb1d9632bcd Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Thu, 18 Apr 2024 23:46:02 +0900 Subject: [PATCH 13/30] add Inline DSL functions. --- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 1 + src/Avalonia.FuncUI/DSL/Documents/Inline.fs | 25 +++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/Avalonia.FuncUI/DSL/Documents/Inline.fs diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index 3a4e52a8..0a88a769 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -99,6 +99,7 @@ + diff --git a/src/Avalonia.FuncUI/DSL/Documents/Inline.fs b/src/Avalonia.FuncUI/DSL/Documents/Inline.fs new file mode 100644 index 00000000..68b24cae --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/Documents/Inline.fs @@ -0,0 +1,25 @@ +namespace Avalonia.FuncUI.DSL + +[] +module Inline = + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder + open Avalonia.Controls.Documents + open Avalonia.Media + + type Inline with + static member textDecorations<'t when 't :> Inline>(value: TextDecorationCollection) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Inline.TextDecorationsProperty, value, ValueNone) + + static member textDecorations<'t when 't :> Inline>(textDecorations: TextDecoration list) : IAttr<'t> = + TextDecorationCollection(textDecorations) |> Inline.textDecorations + + static member textDecorations<'t when 't :> Inline>(textDecorationViews: IView list) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.TextDecorations + let getter : ('t -> obj) = (fun control -> control.TextDecorations :> obj) + let textDecorationViews = textDecorationViews |> List.map (fun x -> x :> IView) + AttrBuilder<'t>.CreateContentMultiple(name, ValueSome getter, ValueNone, textDecorationViews) + + static member baselineAlignment<'t when 't :> Inline>(value: BaselineAlignment) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Inline.BaselineAlignmentProperty, value, ValueNone) + \ No newline at end of file From e3e4dc3f86cfed19d332db7d5ee4a6a366762496 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 00:28:49 +0900 Subject: [PATCH 14/30] add TextDecoration DSL functions. --- .../DSL/Documents/TextDecoration.fs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/Avalonia.FuncUI/DSL/Documents/TextDecoration.fs diff --git a/src/Avalonia.FuncUI/DSL/Documents/TextDecoration.fs b/src/Avalonia.FuncUI/DSL/Documents/TextDecoration.fs new file mode 100644 index 00000000..8678eba0 --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/Documents/TextDecoration.fs @@ -0,0 +1,77 @@ +namespace Avalonia.FuncUI.DSL + +[] +module TextDecoration = + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder + open Avalonia.Media + open Avalonia.Media.Immutable + open Avalonia.Collections + + let create(attrs: IAttr list): IView = + ViewBuilder.Create(attrs) + + module private Internals = + open System.Linq + let patchStrokeDashArray (strokeDashArray: AvaloniaList) (newValues: float seq) = + if Seq.isEmpty newValues then + strokeDashArray.Clear() + else + strokeDashArray |> Seq.except newValues |> strokeDashArray.RemoveAll + + for newIndex, value in Seq.indexed newValues do + let oldValue = strokeDashArray |> Seq.tryFindIndex (fun x -> x = value) + + match oldValue with + | Some oldValue when oldValue = newIndex -> () + | Some oldValue -> strokeDashArray.Move(oldValue, newIndex) + | None -> strokeDashArray.Insert(newIndex, value) + + let compareStrokeDashArray<'e when 'e :> float seq> (a: obj) (b: obj) = + let a = a :?> 'e + let b = b :?> 'e + + Enumerable.SequenceEqual(a, b) + + type TextDecoration with + static member location<'t when 't :> TextDecoration>(value: TextDecorationLocation) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.LocationProperty, value, ValueNone) + + static member stroke<'t when 't :> TextDecoration>(value: IBrush) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeProperty, value, ValueNone) + + static member stroke<'t when 't :> TextDecoration>(s: string) : IAttr<'t> = + SolidColorBrush.Parse s |> TextDecoration.stroke + + static member stroke<'t when 't :> TextDecoration>(color: Color) : IAttr<'t> = + ImmutableSolidColorBrush color |> TextDecoration.stroke + + static member strokeThicknessUnit<'t when 't :> TextDecoration>(value: TextDecorationUnit) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeThicknessUnitProperty, value, ValueNone) + + static member strokeDashArray<'t when 't :> TextDecoration>(value: AvaloniaList) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty>(TextDecoration.StrokeDashArrayProperty, value, ValueNone) + + static member strokeDashArray<'t when 't :> TextDecoration>(value: float list) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.StrokeDashArray + let getter: 't -> float list = (fun control -> Seq.toList control.StrokeDashArray) + let setter: 't * float list -> unit = (fun (control, value) -> Internals.patchStrokeDashArray control.StrokeDashArray value) + let compare: 'obj * 'obj -> bool = fun (a, b) -> Internals.compareStrokeDashArray a b + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty(name, value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + static member strokeDashOffset<'t when 't :> TextDecoration>(value: float) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeDashOffsetProperty, value, ValueNone) + + static member strokeThickness<'t when 't :> TextDecoration>(value: float) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeThicknessProperty, value, ValueNone) + + static member strokeLineCap<'t when 't :> TextDecoration>(value: PenLineCap) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeLineCapProperty, value, ValueNone) + + static member strokeOffset<'t when 't :> TextDecoration>(value: double) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeOffsetProperty, value, ValueNone) + + static member strokeOffsetUnit<'t when 't :> TextDecoration>(value: TextDecorationUnit) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextDecoration.StrokeOffsetUnitProperty, value, ValueNone) From 305d1b8150f523625cad41f4751e3082c0a7afcb Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 07:51:39 +0900 Subject: [PATCH 15/30] add TextBlock DSL functions. --- src/Avalonia.FuncUI/DSL/TextBlock.fs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Avalonia.FuncUI/DSL/TextBlock.fs b/src/Avalonia.FuncUI/DSL/TextBlock.fs index 4cc1461e..83ef1907 100644 --- a/src/Avalonia.FuncUI/DSL/TextBlock.fs +++ b/src/Avalonia.FuncUI/DSL/TextBlock.fs @@ -4,9 +4,14 @@ open Avalonia.Media.Immutable [] module TextBlock = open Avalonia + open Avalonia.Automation + open Avalonia.Automation.Peers open Avalonia.Controls open Avalonia.Controls.Documents + open Avalonia.Layout open Avalonia.Media + open Avalonia.Media.TextFormatting + open Avalonia.Metadata open Avalonia.FuncUI.Builder open Avalonia.FuncUI.Types @@ -27,6 +32,9 @@ module TextBlock = static member background<'t when 't :> TextBlock>(color: Color) : IAttr<'t> = color |> ImmutableSolidColorBrush |> TextBlock.background + static member baselineOffset<'t when 't :> TextBlock>(value: double) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBlock.BaselineOffsetProperty, value, ValueNone) + static member fontFamily<'t when 't :> TextBlock>(value: FontFamily) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBlock.FontFamilyProperty, value, ValueNone) @@ -39,6 +47,9 @@ module TextBlock = static member fontWeight<'t when 't :> TextBlock>(value: FontWeight) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBlock.FontWeightProperty, value, ValueNone) + static member fontStretch<'t when 't :> TextBlock>(value: FontStretch) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBlock.FontStretchProperty, value, ValueNone) + static member foreground<'t when 't :> TextBlock>(value: IBrush) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBlock.ForegroundProperty, value, ValueNone) @@ -58,6 +69,9 @@ module TextBlock = static member lineHeight<'t when 't :> TextBlock>(value: float) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBlock.LineHeightProperty, value, ValueNone) + static member lineSpacing<'t when 't :> TextBlock>(value: float) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBlock.LetterSpacingProperty, value, ValueNone) + static member maxLines<'t when 't :> TextBlock>(value: int) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBlock.MaxLinesProperty, value, ValueNone) From 540dc68ce694f61a2f2c9497cf4db016f8798cd5 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 08:01:16 +0900 Subject: [PATCH 16/30] add Image DSL functions. --- src/Avalonia.FuncUI/DSL/Image.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FuncUI/DSL/Image.fs b/src/Avalonia.FuncUI/DSL/Image.fs index 7477dbf8..cb6ff84c 100644 --- a/src/Avalonia.FuncUI/DSL/Image.fs +++ b/src/Avalonia.FuncUI/DSL/Image.fs @@ -16,4 +16,7 @@ module Image = AttrBuilder<'t>.CreateProperty(Image.SourceProperty, value, ValueNone) static member stretch<'t when 't :> Image>(value: Stretch) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Image.StretchProperty, value, ValueNone) \ No newline at end of file + AttrBuilder<'t>.CreateProperty(Image.StretchProperty, value, ValueNone) + + static member stretchDirection<'t when 't :> Image>(value: StretchDirection) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Image.StretchDirectionProperty, value, ValueNone) From 4230295a0ad2854de6613d1c72d70ba9adbf5f30 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:30:41 +0900 Subject: [PATCH 17/30] move stryles DSL into StyledElement.fs --- .../Avalonia.FuncUI.UnitTests.fsproj | 1 - .../DSL/Base/IStyleHostTests.fs | 109 ------------------ .../DSL/Base/StyledElementTests.fs | 103 ++++++++++++++++- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 2 +- src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs | 74 ------------ src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 48 ++++++++ src/Avalonia.FuncUI/Helpers.fs | 26 +++++ 7 files changed, 175 insertions(+), 188 deletions(-) delete mode 100644 src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs delete mode 100644 src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs diff --git a/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj b/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj index e4e85b56..d93c0ee1 100644 --- a/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj +++ b/src/Avalonia.FuncUI.UnitTests/Avalonia.FuncUI.UnitTests.fsproj @@ -16,7 +16,6 @@ - diff --git a/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs b/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs deleted file mode 100644 index 84752426..00000000 --- a/src/Avalonia.FuncUI.UnitTests/DSL/Base/IStyleHostTests.fs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Avalonia.FuncUI.UnitTests.DSL - -open Avalonia.Controls -open Avalonia.Styling -open global.Xunit - -module IStyleHostTests = - open Avalonia.FuncUI.VirtualDom - open Avalonia.FuncUI.DSL - open Avalonia.FuncUI.Types - - let twoAttrs<'x, 't> (attr: 'x -> IAttr<'t>) a b = - [ attr a :> IAttr ], [ attr b :> IAttr ] - - - let initStyle () = - let s = Style(fun x -> x.Is()) - s.Setters.Add(Setter(Control.TagProperty, "foo")) - s :> IStyle - - [] - let ``styles equality with style list has same style instance`` () = - let style = initStyle () - - let styleList () = [ style ] - - let styles1 = styleList () - let styles2 = styleList () - - let styleList = - (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes - - Assert.Empty styleList - - - [] - let ``styles equality with style list has different style instance`` () = - - let style1 = initStyle () - let style2 = initStyle () - - let styleList = - ([ style1 ], [ style2 ]) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes - - match Assert.Single styleList with - | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } - Value = Some(:? list as [ value ]) } -> - Assert.Equal("Styles", propName) - Assert.NotEqual(style1, value) - Assert.Equal(style2, value) - - | _ -> Assert.Fail $"Not expected delta\n{styleList}" - - [] - let ``styles equality with Styles property has same instance`` () = - let style = initStyle () - - let styles = Styles() - styles.Add style - - let styles1 = styles - let styles2 = styles - - let styleList = - (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes - - Assert.Empty styleList - - [] - let ``styles equality with Styles property has different Styles instance has same style instance`` () = - let style = initStyle () - - let styles1 = Styles() - styles1.Add style - styles1.Resources.Add("key", "value") - - let styles2 = Styles() - styles2.Add style - styles2.Resources.Add("key", "value") - - let styleList = - (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes - - Assert.Empty styleList - - [] - let ``styles equality with Styles property has different Styles instance has different style instance`` () = - let style1 = initStyle () - let style2 = initStyle () - - let styles1 = Styles() - styles1.Add style1 - styles1.Resources.Add("key", "value") - - let styles2 = Styles() - styles2.Add style2 - styles2.Resources.Add("key", "value") - - let styleList = - (styles1, styles2) ||> twoAttrs IStyleHost.styles |> Differ.diffAttributes - - match Assert.Single styleList with - | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } - Value = Some(:? Styles as value) } -> - Assert.Equal("Styles", propName) - Assert.NotEqual(styles1, value) - Assert.Equal(styles2, value) - - | _ -> Assert.Fail $"Not expected delta\n{styleList}" diff --git a/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs b/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs index 4121975f..44782d1c 100644 --- a/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs +++ b/src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs @@ -1,5 +1,6 @@ namespace Avalonia.FuncUI.UnitTests.DSL +open Avalonia open Avalonia.Controls open global.Xunit @@ -7,6 +8,7 @@ module StyledElementTests = open Avalonia.FuncUI.VirtualDom open Avalonia.FuncUI.DSL open Avalonia.FuncUI.Types + open Avalonia.Styling let twoAttrs<'x, 't> (attr: 'x -> IAttr<'t>) a b = [ attr a :> IAttr ], [ attr b :> IAttr ] @@ -20,7 +22,7 @@ module StyledElementTests = let stringList = (classes1, classes2) - ||> twoAttrs Control.classes + ||> twoAttrs StyledElement.classes |> Differ.diffAttributes Assert.Empty stringList @@ -32,7 +34,7 @@ module StyledElementTests = classes.Add "class2" let sameClassesInstance = - (classes, classes) ||> twoAttrs Control.classes |> Differ.diffAttributes + (classes, classes) ||> twoAttrs StyledElement.classes |> Differ.diffAttributes Assert.Empty sameClassesInstance @@ -47,6 +49,101 @@ module StyledElementTests = classes2.Add "class2" let differentClassesInstance = - (classes1, classes2) ||> twoAttrs Control.classes |> Differ.diffAttributes + (classes1, classes2) ||> twoAttrs StyledElement.classes |> Differ.diffAttributes Assert.Empty differentClassesInstance + + let initStyle () = + let s = Style(fun x -> x.Is()) + s.Setters.Add(Setter(Control.TagProperty, "foo")) + s :> IStyle + + [] + let ``styles equality with style list has same style instance`` () = + let style = initStyle () + + let styleList () = [ style ] + + let styles1 = styleList () + let styles2 = styleList () + + let styleList = + (styles1, styles2) ||> twoAttrs StyledElement.styles |> Differ.diffAttributes + + Assert.Empty styleList + + + [] + let ``styles equality with style list has different style instance`` () = + + let style1 = initStyle () + let style2 = initStyle () + + let styleList = + ([ style1 ], [ style2 ]) ||> twoAttrs StyledElement.styles |> Differ.diffAttributes + + match Assert.Single styleList with + | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } + Value = Some(:? list as [ value ]) } -> + Assert.Equal("Styles", propName) + Assert.NotEqual(style1, value) + Assert.Equal(style2, value) + + | _ -> Assert.Fail $"Not expected delta\n{styleList}" + + [] + let ``styles equality with Styles property has same instance`` () = + let style = initStyle () + + let styles = Styles() + styles.Add style + + let styles1 = styles + let styles2 = styles + + let styleList = + (styles1, styles2) ||> twoAttrs StyledElement.styles |> Differ.diffAttributes + + Assert.Empty styleList + + [] + let ``styles equality with Styles property has different Styles instance has same style instance`` () = + let style = initStyle () + + let styles1 = Styles() + styles1.Add style + styles1.Resources.Add("key", "value") + + let styles2 = Styles() + styles2.Add style + styles2.Resources.Add("key", "value") + + let styleList = + (styles1, styles2) ||> twoAttrs StyledElement.styles |> Differ.diffAttributes + + Assert.Empty styleList + + [] + let ``styles equality with Styles property has different Styles instance has different style instance`` () = + let style1 = initStyle () + let style2 = initStyle () + + let styles1 = Styles() + styles1.Add style1 + styles1.Resources.Add("key", "value") + + let styles2 = Styles() + styles2.Add style2 + styles2.Resources.Add("key", "value") + + let styleList = + (styles1, styles2) ||> twoAttrs StyledElement.styles |> Differ.diffAttributes + + match Assert.Single styleList with + | Delta.AttrDelta.Property { Accessor = InstanceProperty { Name = propName } + Value = Some(:? Styles as value) } -> + Assert.Equal("Styles", propName) + Assert.NotEqual(styles1, value) + Assert.Equal(styles2, value) + + | _ -> Assert.Fail $"Not expected delta\n{styleList}" diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index 0a88a769..f4258464 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -61,7 +61,6 @@ - @@ -99,6 +98,7 @@ + diff --git a/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs b/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs deleted file mode 100644 index 01627a6e..00000000 --- a/src/Avalonia.FuncUI/DSL/Base/IStyleHost.fs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Avalonia.FuncUI.DSL - -[] -module IStyleHost = - open Avalonia.FuncUI.Types - open Avalonia.FuncUI.Builder - open Avalonia.Styling - - - module private Internals = - - let patchStyles (styles: Styles) (newValues: IStyle seq) = - let newValues = newValues |> Seq.toList - - if List.isEmpty newValues then - styles.Clear() - else - styles |> Seq.except newValues |> styles.RemoveAll - - for newIndex, newStyle in List.indexed newValues do - let oldIndex = styles |> Seq.tryFindIndex ((=) newStyle) - - match oldIndex with - | Some oldIndex when oldIndex = newIndex -> () - | Some oldIndex -> styles.Move(oldIndex, newIndex) - | None -> styles.Insert(newIndex, newStyle) - - type IStyleHost with - - /// Use 'classes' instead when possible. - static member styles<'t when 't :> IStyleHost>(value: Styles) : IAttr<'t> = - let getter: ('t -> Styles) = (fun control -> control.Styles) - - let setter: ('t * Styles -> unit) = - (fun (control, value) -> - Internals.patchStyles control.Styles value - control.Styles.Resources <- value.Resources) - - let compare: (obj * obj -> bool) = - fun (a, b) -> - match a, b with - | (:? Styles as a), (:? Styles as b) -> - System.Linq.Enumerable.SequenceEqual(a, b) - && System.Linq.Enumerable.SequenceEqual(a.Resources, b.Resources) - && System.Linq.Enumerable.SequenceEqual( - a.Resources.MergedDictionaries, - b.Resources.MergedDictionaries - ) - && System.Linq.Enumerable.SequenceEqual( - a.Resources.ThemeDictionaries, - b.Resources.ThemeDictionaries - ) - | _ -> a = b - - let factory = fun () -> Styles() - - AttrBuilder<'t>.CreateProperty("Styles", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) - - /// Use 'classes' instead when possible. - static member styles<'t when 't :> IStyleHost>(styles: IStyle list) : IAttr<'t> = - let getter: ('t -> (IStyle list)) = (fun control -> control.Styles |> Seq.toList) - - let setter: ('t * IStyle list -> unit) = - (fun (control, value) -> Internals.patchStyles control.Styles value) - - let compare: (obj * obj -> bool) = - fun (a, b) -> - match a, b with - | (:? list as a), (:? list as b) -> System.Linq.Enumerable.SequenceEqual(a, b) - | _ -> a = b - - let factory = fun () -> [] - - AttrBuilder<'t>.CreateProperty("Styles", styles, ValueSome getter, ValueSome setter, ValueSome compare, factory) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index fdc8ed6e..2af6e4d2 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -5,6 +5,7 @@ open Avalonia.Controls.Primitives [] module StyledElement = open Avalonia + open Avalonia.FuncUI open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Styling @@ -192,6 +193,53 @@ module StyledElement = AttrBuilder<'t>.CreateProperty("Classes", classes, ValueSome getter, ValueSome setter, ValueSome compare, factory) + /// Use 'classes' instead when possible. + static member styles<'t when 't :> StyledElement>(value: Styles) : IAttr<'t> = + let getter: ('t -> Styles) = (fun control -> control.Styles) + + let setter: ('t * Styles -> unit) = + (fun (control, value) -> + Setters.avaloniaList control.Styles value + control.Styles.Resources <- value.Resources) + + let compare: (obj * obj -> bool) = + fun (a, b) -> + match a, b with + | (:? Styles as a), (:? Styles as b) -> + System.Linq.Enumerable.SequenceEqual(a, b) + && System.Linq.Enumerable.SequenceEqual(a.Resources, b.Resources) + && System.Linq.Enumerable.SequenceEqual( + a.Resources.MergedDictionaries, + b.Resources.MergedDictionaries + ) + && System.Linq.Enumerable.SequenceEqual( + a.Resources.ThemeDictionaries, + b.Resources.ThemeDictionaries + ) + | _ -> a = b + + let factory = fun () -> Styles() + + AttrBuilder<'t>.CreateProperty("Styles", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + /// Use 'classes' instead when possible. + static member styles<'t when 't :> StyledElement>(styles: IStyle list) : IAttr<'t> = + let getter: ('t -> (IStyle list)) = (fun control -> control.Styles |> Seq.toList) + + let setter: ('t * IStyle list -> unit) = + (fun (control, value) -> Setters.avaloniaList control.Styles value) + + let compare: (obj * obj -> bool) = + fun (a, b) -> + match a, b with + | (:? list as a), (:? list as b) -> System.Linq.Enumerable.SequenceEqual(a, b) + | _ -> a = b + + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty("Styles", styles, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + static member resources<'t when 't :> StyledElement>(value: IResourceDictionary) : IAttr<'t> = let getter : ('t -> IResourceDictionary) = (fun control -> control.Resources) let setter : ('t * IResourceDictionary -> unit) = (fun (control, value) -> control.Resources <- value) diff --git a/src/Avalonia.FuncUI/Helpers.fs b/src/Avalonia.FuncUI/Helpers.fs index 91e7fc7f..b488da52 100644 --- a/src/Avalonia.FuncUI/Helpers.fs +++ b/src/Avalonia.FuncUI/Helpers.fs @@ -18,8 +18,31 @@ module AvaloniaExtensions = let style = StyleInclude(baseUri = null) style.Source <- Uri(source) this.Add(style) + +module internal Setters = + open Avalonia.Collections + open Avalonia.Controls + + /// Update list with minimal CollectionChanged. + let avaloniaList<'t when 't : equality> (list: IAvaloniaList<'t>) (newItems: seq<'t>) = + if Seq.isEmpty newItems then + list.Clear() + else if list.Count = 0 then + list.AddRange(newItems) + else + list |> Seq.except newItems |> list.RemoveAll + for newIndex, newItem in Seq.indexed newItems do + let oldIndex = list |> Seq.tryFindIndex ((=) newItem) + + match oldIndex with + | Some oldIndex when oldIndex = newIndex -> () + | Some oldIndex -> list.Move(oldIndex, newIndex) + | None -> list.Insert(newIndex, newItem) + + module internal EqualityComparers = + open System.Linq open Avalonia.Media let compareTransforms (t1: obj, t2: obj) = @@ -27,3 +50,6 @@ module internal EqualityComparers = | :? ITransform as t1, (:? ITransform as t2) when t1.GetType() = t2.GetType() -> t1.Value.Equals(t2.Value) | _ -> false + + let compareSeq<'e,'t when 'e :> 't seq> (a: obj, b: obj) = + Enumerable.SequenceEqual(a :?> 'e, b :?> 'e) From 11bd931155bd24eaa174ef650ebf1292919df9ba Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 18:07:06 +0900 Subject: [PATCH 18/30] add Flyout DSL functions. --- src/Avalonia.FuncUI/DSL/Flyout.fs | 166 ++++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Flyout.fs b/src/Avalonia.FuncUI/DSL/Flyout.fs index 871fce92..5306c0cf 100644 --- a/src/Avalonia.FuncUI/DSL/Flyout.fs +++ b/src/Avalonia.FuncUI/DSL/Flyout.fs @@ -1,31 +1,127 @@ namespace Avalonia.FuncUI.DSL +open Avalonia +open Avalonia.Controls +open Avalonia.Controls.Diagnostics +open Avalonia.Controls.Primitives +open Avalonia.Controls.Primitives.PopupPositioning open Avalonia.Controls.Templates +open Avalonia.Input +open Avalonia.Styling [] module Flyout = - open Avalonia.Controls + open System + open System.Threading + open System.ComponentModel + open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder - open Avalonia.Controls.Primitives let create (attrs: IAttr list): IView = ViewBuilder.Create(attrs) + + type FlyoutBase with + static member onOpened<'t when 't :> FlyoutBase>(func: 't -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.Opened + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let hander = EventHandler(fun s _ -> s :?> 't |> func) + let event = control.Opened + + event.AddHandler(hander) + token.Register(fun () -> event.RemoveHandler(hander)) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onClosed<'t when 't :> FlyoutBase>(func: 't -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.Closed + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let hander = EventHandler(fun s _ -> s :?> 't |> func) + let event = control.Closed + + event.AddHandler(hander) + token.Register(fun () -> event.RemoveHandler(hander)) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + + type Control with + static member attachedFlyout<'t when 't :> Control>(value: FlyoutBase) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(FlyoutBase.AttachedFlyoutProperty, value, ValueNone) type PopupFlyoutBase with /// /// A value indicating how the flyout is positioned. /// - static member placement<'t when 't :> FlyoutBase>(value: PlacementMode) : IAttr<'t> = + static member placement<'t when 't :> PopupFlyoutBase>(value: PlacementMode) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.PlacementProperty, value, ValueNone) + static member horizontalOffset<'t when 't :> PopupFlyoutBase>(value: double) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.HorizontalOffsetProperty, value, ValueNone) + + static member verticalOffset<'t when 't :> PopupFlyoutBase>(value: double) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.VerticalOffsetProperty, value, ValueNone) + + static member placementAnchor<'t when 't :> PopupFlyoutBase>(value: PopupAnchor) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.PlacementAnchorProperty, value, ValueNone) + + static member placementGravity<'t when 't :> PopupFlyoutBase>(value: PopupGravity) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.PlacementGravityProperty, value, ValueNone) + /// /// A value indicating flyout show mode. /// - static member showMode<'t when 't :> FlyoutBase>(value: FlyoutShowMode) : IAttr<'t> = + static member showMode<'t when 't :> PopupFlyoutBase>(value: FlyoutShowMode) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.ShowModeProperty, value, ValueNone) + static member overlayInputPassThroughElement<'t when 't :> PopupFlyoutBase>(value: IInputElement) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(PopupFlyoutBase.OverlayInputPassThroughElementProperty, value, ValueNone) + + static member onPopupHostChanged<'t when 't :> PopupFlyoutBase>(func: IPopupHost voption -> unit, ?subPatchOptions) : IAttr<'t> = + let name = (nameof Unchecked.defaultof.add_PopupHostChanged).Substring(4) + let factory: AvaloniaObject * (IPopupHost voption -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = (control :?> 't) :> IPopupHostProvider + let hander (host:IPopupHost) = + match host with + | null -> func ValueNone + | host -> func (ValueSome host) + + control.add_PopupHostChanged hander + token.Register(fun () -> control.remove_PopupHostChanged hander) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onOpening<'t when 't :> PopupFlyoutBase>(func: 't -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.Opening + let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let hander = EventHandler(fun s _ -> s :?> 't |> func) + let event = control.Opening + + event.AddHandler(hander) + token.Register(fun () -> event.RemoveHandler(hander)) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onClosing<'t when 't :> PopupFlyoutBase>(func: 't * CancelEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.Closing + let factory: AvaloniaObject * ('t * CancelEventArgs -> unit) * CancellationToken -> unit = + (fun (control, func, token) -> + let control = control :?> 't + let hander = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let event = control.Closing + + event.AddHandler(hander) + token.Register(fun () -> event.RemoveHandler(hander)) |> ignore) + + AttrBuilder<'t>.CreateSubscription(name, factory, func, ?subPatchOptions = subPatchOptions) + type Flyout with static member content<'t when 't :> Flyout>(value: IView option) : IAttr<'t> = AttrBuilder<'t>.CreateContentSingle(Flyout.ContentProperty, value) @@ -38,19 +134,36 @@ module Flyout = /// /// A value indicating flyout style classes to apply. See https://docs.avaloniaui.net/docs/controls/flyouts#styling-flyouts /// - static member flyoutPresenterClasses<'t when 't :> Flyout>(value: string list) : IAttr<'t> = - let getter : ('t -> string list) = (fun control -> control.FlyoutPresenterClasses |> Seq.map id |> List.ofSeq) + static member flyoutPresenterClasses<'t when 't :> Flyout>(value: Classes) : IAttr<'t> = + let getter : ('t -> Classes) = (fun control -> control.FlyoutPresenterClasses) + let setter : ('t * Classes -> unit) = (fun (control, value) -> + ClassesInternals.patchStandardClasses control.FlyoutPresenterClasses value) + + let compare = ClassesInternals.compareClasses + + let factory = (fun () -> Classes()) + + AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + /// + /// A value indicating flyout style classes to apply. See https://docs.avaloniaui.net/docs/controls/flyouts#styling-flyouts + /// + static member flyoutPresenterClasses<'t when 't :> Flyout>(classes: string list) : IAttr<'t> = + let getter : ('t -> string list) = (fun control -> Seq.toList control.FlyoutPresenterClasses) let setter : ('t * string list -> unit) = (fun (control, value) -> - control.FlyoutPresenterClasses.Clear() - control.FlyoutPresenterClasses.AddRange(value)) + ClassesInternals.patchStandardClasses control.FlyoutPresenterClasses value) + + let compare = ClassesInternals.compareClasses - AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", value, ValueSome getter, ValueSome setter, ValueNone) + AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", classes, ValueSome getter, ValueSome setter, ValueSome compare) + static member flyoutPresenterTheme<'t when 't :> Flyout>(theme: ControlTheme) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Flyout.FlyoutPresenterThemeProperty, theme, ValueNone) [] module MenuFlyout = open System.Collections - open Avalonia.Controls + open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder @@ -70,13 +183,28 @@ module MenuFlyout = static member itemTemplate<'t when 't :> MenuFlyout>(value : IDataTemplate): IAttr<'t> = AttrBuilder<'t>.CreateProperty(MenuFlyout.ItemTemplateProperty, value, ValueNone) - /// - /// A value indicating menu flyout style classes to apply. See https://docs.avaloniaui.net/docs/controls/flyouts#styling-flyouts - /// - static member flyoutPresenterClasses<'t when 't :> MenuFlyout>(value: string list) : IAttr<'t> = - let getter : ('t -> string list) = (fun control -> control.FlyoutPresenterClasses |> Seq.map id |> List.ofSeq) - let setter : ('t * string list -> unit) = (fun (control, value) -> - control.FlyoutPresenterClasses.Clear() - control.FlyoutPresenterClasses.AddRange(value)) + static member itemContainerTheme<'t when 't :> MenuFlyout>(theme: ControlTheme) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(MenuFlyout.ItemContainerThemeProperty, theme, ValueNone) - AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", value, ValueSome getter, ValueSome setter, ValueNone) + static member flyoutPresenterTheme<'t when 't :> MenuFlyout>(theme: ControlTheme) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(MenuFlyout.FlyoutPresenterThemeProperty, theme, ValueNone) + + static member flyoutPresenterClasses<'t when 't :> MenuFlyout>(value: Classes) : IAttr<'t> = + let getter : ('t -> Classes) = (fun control -> control.FlyoutPresenterClasses) + let setter : ('t * Classes -> unit) = (fun (control, value) -> + ClassesInternals.patchStandardClasses control.FlyoutPresenterClasses value) + + let compare = ClassesInternals.compareClasses + + let factory = (fun () -> Classes()) + + AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", value, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + static member flyoutPresenterClasses<'t when 't :> MenuFlyout>(classes: string list) : IAttr<'t> = + let getter : ('t -> string list) = (fun control -> Seq.toList control.FlyoutPresenterClasses) + let setter : ('t * string list -> unit) = (fun (control, value) -> + ClassesInternals.patchStandardClasses control.FlyoutPresenterClasses value) + + let compare = ClassesInternals.compareClasses + + AttrBuilder<'t>.CreateProperty("FlyoutPresenterClasses", classes, ValueSome getter, ValueSome setter, ValueSome compare) From 727682103b458563581339328616d2c231a6fd86 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 18:53:00 +0900 Subject: [PATCH 19/30] refactor subscription function if passing event source, to use AddHandler/RemoveHandler. --- src/Avalonia.FuncUI/DSL/Base/Layoutable.fs | 7 +++-- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 28 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs b/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs index 90c14a18..b19553af 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Layoutable.fs @@ -7,6 +7,7 @@ module Layoutable = open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder open Avalonia.Layout + open System type Layoutable with static member onEffectiveViewportChanged<'t when 't :> Layoutable>(func: EffectiveViewportChangedEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = @@ -25,9 +26,11 @@ module Layoutable = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let disposable = control.LayoutUpdated.Subscribe(fun _ -> func control) + let handler = EventHandler(fun s _ -> func (s :?> 't)) + let event = control.LayoutUpdated - token.Register(fun () -> disposable.Dispose()) |> ignore) + event.AddHandler(handler) + token.Register(fun () -> event.RemoveHandler(handler)) |> ignore) AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index 2af6e4d2..e2f5bc24 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -11,6 +11,7 @@ module StyledElement = open Avalonia.Styling open Avalonia.LogicalTree open System.Threading + open System module internal ClassesInternals = open System.Linq @@ -116,10 +117,11 @@ module StyledElement = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let disposable = - control.DataContextChanged.Subscribe(fun _ -> func control) - - token.Register(fun () -> disposable.Dispose()) |> ignore) + let hander = EventHandler(fun s e -> func (s :?> 't)) + let event = control.DataContextChanged + + event.AddHandler(hander) + token.Register(fun () -> event.RemoveHandler(hander)) |> ignore) AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) @@ -128,9 +130,11 @@ module StyledElement = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let disposable = control.Initialized.Subscribe(fun _ -> func control) + let handler = EventHandler(fun s e -> func (s :?> 't)) + let event = control.Initialized - token.Register(fun () -> disposable.Dispose()) |> ignore) + event.AddHandler(handler) + token.Register(fun () -> event.RemoveHandler(handler)) |> ignore) AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) @@ -139,9 +143,11 @@ module StyledElement = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let disposable = control.ResourcesChanged.Subscribe(fun _ -> func control) + let handler = EventHandler<_>(fun s e -> func (s :?> 't)) + let event = control.ResourcesChanged - token.Register(fun () -> disposable.Dispose()) |> ignore) + event.AddHandler(handler) + token.Register(fun () -> event.RemoveHandler(handler)) |> ignore) AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) @@ -150,9 +156,11 @@ module StyledElement = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let disposable = control.ActualThemeVariantChanged.Subscribe(fun _ -> func control) + let handler = EventHandler(fun s e -> func (s :?> 't)) + let event = control.ActualThemeVariantChanged - token.Register(fun () -> disposable.Dispose()) |> ignore) + event.AddHandler(handler) + token.Register(fun () -> event.RemoveHandler(handler)) |> ignore) AttrBuilder<'t>.CreateSubscription<'t>(name, factory, func, ?subPatchOptions = subPatchOptions) From 3f007086bd311ecf57ff587ef296781fd0b9d1ae Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:04:47 +0900 Subject: [PATCH 20/30] add TemplatedControl bindings. --- .../DSL/Primitives/TemplatedControl.fs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Primitives/TemplatedControl.fs b/src/Avalonia.FuncUI/DSL/Primitives/TemplatedControl.fs index fbd30d85..9d7e763e 100644 --- a/src/Avalonia.FuncUI/DSL/Primitives/TemplatedControl.fs +++ b/src/Avalonia.FuncUI/DSL/Primitives/TemplatedControl.fs @@ -1,19 +1,27 @@ namespace Avalonia.FuncUI.DSL [] -module TemplatedControl = - open Avalonia.Media.Immutable +module TemplatedControl = open Avalonia - open Avalonia.FuncUI.Types - open Avalonia.FuncUI.Builder - open Avalonia.Media + open Avalonia.Controls open Avalonia.Controls.Primitives open Avalonia.Controls.Templates + open Avalonia.Media + open Avalonia.Media.Immutable + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder let create (attrs: IAttr list): IView = ViewBuilder.Create(attrs) - + + type Control with + static member isTemplateFocusTarget<'t when 't :> Control>(value: bool) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TemplatedControl.IsTemplateFocusTargetProperty, value, ValueNone) + type TemplatedControl with + static member onTemplateApplied<'t when 't :> TemplatedControl>(func: TemplateAppliedEventArgs -> unit, ?subPatchOptions) : IAttr<'t> = + AttrBuilder<'t>.CreateSubscription(TemplatedControl.TemplateAppliedEvent, func, ?subPatchOptions = subPatchOptions) + static member background<'t when 't :> TemplatedControl>(value: IBrush) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TemplatedControl.BackgroundProperty, value, ValueNone) @@ -55,7 +63,10 @@ module TemplatedControl = static member fontWeight<'t when 't :> TemplatedControl>(value: FontWeight) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TemplatedControl.FontWeightProperty, value, ValueNone) - + + static member fontStretch<'t when 't :> TemplatedControl>(value: FontStretch) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TemplatedControl.FontStretchProperty, value, ValueNone) + static member foreground<'t when 't :> TemplatedControl>(value: IBrush) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TemplatedControl.ForegroundProperty, value, ValueNone) @@ -88,6 +99,3 @@ module TemplatedControl = static member template<'t when 't :> TemplatedControl>(value: IControlTemplate) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TemplatedControl.TemplateProperty, value, ValueNone) - - static member isTemplateFocusTarget<'t when 't :> TemplatedControl>(value: bool) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(TemplatedControl.IsTemplateFocusTargetProperty, value, ValueNone) \ No newline at end of file From 86e8af8e288624e7e934cfbe13acf4c832633a86 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:42:41 +0900 Subject: [PATCH 21/30] add TextBox bindings. --- src/Avalonia.FuncUI/DSL/TextBox.fs | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/Avalonia.FuncUI/DSL/TextBox.fs b/src/Avalonia.FuncUI/DSL/TextBox.fs index cb19f13a..1e1548c6 100644 --- a/src/Avalonia.FuncUI/DSL/TextBox.fs +++ b/src/Avalonia.FuncUI/DSL/TextBox.fs @@ -8,6 +8,7 @@ module TextBox = open Avalonia.Media open Avalonia.FuncUI.Builder open Avalonia.FuncUI.Types + open Avalonia.Interactivity let create (attrs: IAttr list): IView = ViewBuilder.Create(attrs) @@ -68,12 +69,75 @@ module TextBox = static member maxLength<'t when 't :> TextBox>(value: int) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBox.MaxLengthProperty, value, ValueNone) + static member maxLines<'t when 't :> TextBox>(value: int) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.MaxLinesProperty, value, ValueNone) + + static member letterSpacing<'t when 't :> TextBox>(value: float) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.LetterSpacingProperty, value, ValueNone) + + static member lineHeight<'t when 't :> TextBox>(value: float) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.LineHeightProperty, value, ValueNone) static member text<'t when 't :> TextBox>(value: string) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBox.TextProperty, value, ValueNone) + static member selectedText<'t when 't :> TextBox>(value: string) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.SelectedText + let getter: 't -> string = fun control -> control.SelectedText + let setter: 't * string -> unit = fun (control, value) -> control.SelectedText <- value + + AttrBuilder<'t>.CreateProperty(name, value, ValueSome getter, ValueSome setter, ValueNone, (fun () -> "")) + + static member innerLeftContent<'t when 't :> TextBox>(text: string) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.InnerLeftContentProperty, text, ValueNone) + + static member innerLeftContent<'t when 't :> TextBox>(value: obj) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.InnerLeftContentProperty, value, ValueNone) + + static member innerLeftContent<'t when 't :> TextBox>(view: IView option) : IAttr<'t> = + AttrBuilder<'t>.CreateContentSingle(TextBox.InnerLeftContentProperty, view) + + static member innerLeftContent<'t when 't :> TextBox>(view: IView) : IAttr<'t> = + Some view |> TextBox.innerLeftContent + + static member innerRightContent<'t when 't :> TextBox>(text: string) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.InnerRightContentProperty, text, ValueNone) + + static member innerRightContent<'t when 't :> TextBox>(value: obj) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.InnerRightContentProperty, value, ValueNone) + + static member innerRightContent<'t when 't :> TextBox>(view: IView option) : IAttr<'t> = + AttrBuilder<'t>.CreateContentSingle(TextBox.InnerRightContentProperty, view) + + static member innerRightContent<'t when 't :> TextBox>(view: IView) : IAttr<'t> = + Some view |> TextBox.innerRightContent + + static member revealPassword<'t when 't :> TextBox>(value: bool) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.RevealPasswordProperty, value, ValueNone) + + static member isUndoEnabled<'t when 't :> TextBox>(value: bool) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.IsUndoEnabledProperty, value, ValueNone) + + static member undoLimit<'t when 't :> TextBox>(value: int) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(TextBox.UndoLimitProperty, value, ValueNone) + + static member onCopyingToClipboard<'t when 't :> TextBox>(func: RoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(TextBox.CopyingToClipboardEvent, func, ?subPatchOptions = subPatchOptions) + + static member onCuttingToClipboard<'t when 't :> TextBox>(func: RoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(TextBox.CuttingToClipboardEvent, func, ?subPatchOptions = subPatchOptions) + + static member onPastingFromClipboard<'t when 't :> TextBox>(func: RoutedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(TextBox.PastingFromClipboardEvent, func, ?subPatchOptions = subPatchOptions) + static member onTextChanged<'t when 't :> TextBox>(func: string -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(TextBox.TextProperty, func, ?subPatchOptions = subPatchOptions) + static member onTextChanged<'t when 't :> TextBox>(func: TextChangedEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(TextBox.TextChangedEvent, func, ?subPatchOptions = subPatchOptions) + + static member onTextChanging<'t when 't :> TextBox>(func: TextChangingEventArgs -> unit, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(TextBox.TextChangingEvent, func, ?subPatchOptions = subPatchOptions) + static member textAlignment<'t when 't :> TextBox>(alignment: TextAlignment) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(TextBox.TextAlignmentProperty, alignment, ValueNone) From 392699b838ebbfa8907122febae2fc7e99826753 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Fri, 19 Apr 2024 23:20:47 +0900 Subject: [PATCH 22/30] add ItemsControl bindings. --- src/Avalonia.FuncUI/DSL/ItemsControl.fs | 56 +++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/ItemsControl.fs b/src/Avalonia.FuncUI/DSL/ItemsControl.fs index f156ad3c..85576b76 100644 --- a/src/Avalonia.FuncUI/DSL/ItemsControl.fs +++ b/src/Avalonia.FuncUI/DSL/ItemsControl.fs @@ -1,12 +1,17 @@ namespace Avalonia.FuncUI.DSL +open System +open System.Collections +open System.Threading + open Avalonia +open Avalonia.Controls +open Avalonia.Controls.Templates +open Avalonia.Data +open Avalonia.Styling [] module ItemsControl = - open Avalonia.Controls.Templates - open System.Collections - open Avalonia.Controls open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder @@ -15,6 +20,9 @@ module ItemsControl = type ItemsControl with + static member displayMemberBinding<'t when 't :> ItemsControl>(value: IBinding) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(ItemsControl.DisplayMemberBindingProperty, value, ValueNone) + static member viewItems<'t when 't :> ItemsControl>(views: IView list) : IAttr<'t> = let getter : ('t -> obj) = (fun control -> control.Items :> obj) AttrBuilder<'t>.CreateContentMultiple("Items", ValueSome getter, ValueNone, views) @@ -22,12 +30,54 @@ module ItemsControl = static member dataItems<'t when 't :> ItemsControl>(data: IEnumerable) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(ItemsControl.ItemsSourceProperty, data, ValueNone) + static member itemContainerTheme<'t when 't :> ItemsControl>(value: ControlTheme) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(ItemsControl.ItemContainerThemeProperty, value, ValueNone) + static member itemsPanel<'t when 't :> ItemsControl>(value: ITemplate) : IAttr<'t> = AttrBuilder<'t>.CreateProperty>(ItemsControl.ItemsPanelProperty, value, ValueNone) static member itemTemplate<'t when 't :> ItemsControl>(value: IDataTemplate) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(ItemsControl.ItemTemplateProperty, value, ValueNone) + static member onContainerPrepared<'t when 't :> ItemsControl>(func: ('t * ContainerPreparedEventArgs) -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.ContainerPrepared + let factory: AvaloniaObject * ('t * ContainerPreparedEventArgs -> unit) * CancellationToken -> unit = + fun (control, func, token) -> + let control = control :?> 't + let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let event = control.ContainerPrepared + + event.AddHandler(handler) + token.Register(fun _ -> event.RemoveHandler(handler)) |> ignore + + AttrBuilder<'t>.CreateSubscription<'t * ContainerPreparedEventArgs>(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onContainerIndexChanged<'t when 't :> ItemsControl>(func: ('t * ContainerIndexChangedEventArgs) -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.ContainerIndexChanged + let factory: AvaloniaObject * ('t * ContainerIndexChangedEventArgs -> unit) * CancellationToken -> unit = + fun (control, func, token) -> + let control = control :?> 't + let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let event = control.ContainerIndexChanged + + event.AddHandler(handler) + token.Register(fun _ -> event.RemoveHandler(handler)) |> ignore + + AttrBuilder<'t>.CreateSubscription<'t * ContainerIndexChangedEventArgs>(name, factory, func, ?subPatchOptions = subPatchOptions) + + static member onContainerClearing<'t when 't :> ItemsControl>(func: ('t * ContainerClearingEventArgs) -> unit, ?subPatchOptions) = + let name = nameof Unchecked.defaultof<'t>.ContainerClearing + let factory: AvaloniaObject * ('t * ContainerClearingEventArgs -> unit) * CancellationToken -> unit = + fun (control, func, token) -> + let control = control :?> 't + let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let event = control.ContainerClearing + + event.AddHandler(handler) + token.Register(fun _ -> event.RemoveHandler(handler)) |> ignore + + AttrBuilder<'t>.CreateSubscription<'t * ContainerClearingEventArgs>(name, factory, func, ?subPatchOptions = subPatchOptions) + static member onItemsChanged<'t when 't :> ItemsControl>(func: IEnumerable -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription( ItemsControl.ItemsSourceProperty :> AvaloniaProperty, From 7d0d13fe9aceb1c77b75c70a83dbd1bf5202c676 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Sat, 20 Apr 2024 00:20:01 +0900 Subject: [PATCH 23/30] Type parameters Modified to explicitly. --- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 2 +- src/Avalonia.FuncUI/DSL/ItemsControl.fs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index e2f5bc24..af4852bb 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -143,7 +143,7 @@ module StyledElement = let factory: AvaloniaObject * ('t -> unit) * CancellationToken -> unit = (fun (control, func, token) -> let control = control :?> 't - let handler = EventHandler<_>(fun s e -> func (s :?> 't)) + let handler = EventHandler(fun s e -> func (s :?> 't)) let event = control.ResourcesChanged event.AddHandler(handler) diff --git a/src/Avalonia.FuncUI/DSL/ItemsControl.fs b/src/Avalonia.FuncUI/DSL/ItemsControl.fs index 85576b76..be89a74b 100644 --- a/src/Avalonia.FuncUI/DSL/ItemsControl.fs +++ b/src/Avalonia.FuncUI/DSL/ItemsControl.fs @@ -44,7 +44,7 @@ module ItemsControl = let factory: AvaloniaObject * ('t * ContainerPreparedEventArgs -> unit) * CancellationToken -> unit = fun (control, func, token) -> let control = control :?> 't - let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let handler = EventHandler(fun s e -> func(s :?> 't, e)) let event = control.ContainerPrepared event.AddHandler(handler) @@ -57,7 +57,7 @@ module ItemsControl = let factory: AvaloniaObject * ('t * ContainerIndexChangedEventArgs -> unit) * CancellationToken -> unit = fun (control, func, token) -> let control = control :?> 't - let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let handler = EventHandler(fun s e -> func(s :?> 't, e)) let event = control.ContainerIndexChanged event.AddHandler(handler) @@ -70,7 +70,7 @@ module ItemsControl = let factory: AvaloniaObject * ('t * ContainerClearingEventArgs -> unit) * CancellationToken -> unit = fun (control, func, token) -> let control = control :?> 't - let handler = EventHandler<_>(fun s e -> func(s :?> 't, e)) + let handler = EventHandler(fun s e -> func(s :?> 't, e)) let event = control.ContainerClearing event.AddHandler(handler) From 410d228f11ae4139f5b12ad7f15e4c9115e5607c Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:09:51 +0900 Subject: [PATCH 24/30] documentation for updating `Classes`' standard classes Expanded the documentation for the `patchStandardClasses` function, which updates the standard classes of `Classes`, with detailed explanations about the mixture of standard classes and pseudoclasses. --- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index af4852bb..c0d825b1 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -19,7 +19,17 @@ module StyledElement = /// pseudoclasse is classe beginning with a ':' character. let isPseudoClass (s: string) = s.StartsWith(":") - /// Update `Classes`'s standard classes with new values. + /// + /// Update `Classes`'s standard classes with new values. + /// + /// + /// + /// `Classes` is mixed standard classes and pseudoclasses(beginning with a ':' character). + /// + /// pseudoclasses may only setting by the control's protected property itself. + /// If set by external, it will be throw exception. + /// Therefore, when updating from the external, it is necessary to avoid setting the pseudoclasses directly. + /// let patchStandardClasses (classes: Classes) (newValues: string seq) = let (|PseudoClass|_|) (s: string) = From 92e67bebf39e98f0bdc2c6fb605bf67bcf29ea3a Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:27:37 +0900 Subject: [PATCH 25/30] fix isPseudoClass - StartsWith ... use Char instead of String. - update comment. --- src/Avalonia.FuncUI/DSL/Base/StyledElement.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs index c0d825b1..247d38ee 100644 --- a/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs +++ b/src/Avalonia.FuncUI/DSL/Base/StyledElement.fs @@ -16,8 +16,8 @@ module StyledElement = module internal ClassesInternals = open System.Linq - /// pseudoclasse is classe beginning with a ':' character. - let isPseudoClass (s: string) = s.StartsWith(":") + /// pseudoclass is beginning with a ':' character. + let isPseudoClass (s: string) = s.StartsWith(':') /// /// Update `Classes`'s standard classes with new values. From 1f1464b2b1d302aa91171d9cf2986a7c9bcd8ca5 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:12:41 +0900 Subject: [PATCH 26/30] move dataTemplates binding functions to Control.fs --- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 1 - src/Avalonia.FuncUI/DSL/Base/Control.fs | 19 +++++++ .../DSL/Base/IDataTemplateHost.fs | 49 ------------------- 3 files changed, 19 insertions(+), 50 deletions(-) delete mode 100644 src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index f4258464..8d2b4ba0 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -63,7 +63,6 @@ - diff --git a/src/Avalonia.FuncUI/DSL/Base/Control.fs b/src/Avalonia.FuncUI/DSL/Base/Control.fs index a598f2dd..a54d1267 100644 --- a/src/Avalonia.FuncUI/DSL/Base/Control.fs +++ b/src/Avalonia.FuncUI/DSL/Base/Control.fs @@ -4,7 +4,9 @@ namespace Avalonia.FuncUI.DSL module Control = open Avalonia.Controls open Avalonia.Controls.Primitives + open Avalonia.Controls.Templates open Avalonia.Interactivity + open Avalonia.FuncUI open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder @@ -19,6 +21,23 @@ module Control = static member tag<'t when 't :> Control>(value: obj) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Control.TagProperty, value, ValueNone) + static member dataTemplates<'t when 't :> Control>(templates: DataTemplates) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.DataTemplates + let getter: 't -> DataTemplates = (fun control -> control.DataTemplates) + let setter: ('t * DataTemplates -> unit) = (fun (control, value) -> Setters.avaloniaList control.DataTemplates value) + let compare: obj * obj -> bool = EqualityComparers.compareSeq + let factory = fun () -> DataTemplates() + + AttrBuilder<'t>.CreateProperty(name, templates, ValueSome getter, ValueSome setter, ValueSome compare, factory) + + static member dataTemplates<'t when 't :> Control>(templates: IDataTemplate list) : IAttr<'t> = + let name = nameof Unchecked.defaultof<'t>.DataTemplates + let getter: 't -> IDataTemplate list = (fun control -> control.DataTemplates |> Seq.toList) + let setter: ('t * IDataTemplate list -> unit) = (fun (control, value) -> Setters.avaloniaList control.DataTemplates value) + let factory = fun () -> [] + + AttrBuilder<'t>.CreateProperty(name, templates, ValueSome getter, ValueSome setter, ValueNone, factory) + static member contextMenu<'t when 't :> Control>(menuView: IView option) : IAttr<'t> = let view = match menuView with diff --git a/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs b/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs deleted file mode 100644 index 78e19ccb..00000000 --- a/src/Avalonia.FuncUI/DSL/Base/IDataTemplateHost.fs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Avalonia.FuncUI.DSL - -[] -module IDataTemplateHost = - open Avalonia.FuncUI.Types - open Avalonia.FuncUI.Builder - open Avalonia.Controls.Templates - - module private Internals = - let patchDataTemplates(dataTemplates: DataTemplates) (templates: IDataTemplate seq) = - - if Seq.isEmpty templates then - dataTemplates.Clear() - else - templates - |> Seq.except dataTemplates - |> dataTemplates.RemoveAll - - for newIndex, template in Seq.indexed templates do - let oldIndex = dataTemplates |> Seq.tryFindIndex template.Equals - - match oldIndex with - | Some oldIndex when oldIndex = newIndex -> () - | Some oldIndex -> dataTemplates.Move(oldIndex, newIndex) - | None -> dataTemplates.Insert(newIndex, template) - - type IDataTemplateHost with - static member dataTemplates<'t when 't :> IDataTemplateHost>(templates: DataTemplates) : IAttr<'t> = - let getter: 't -> DataTemplates = (fun control -> control.DataTemplates) - - let setter: ('t * DataTemplates -> unit) = (fun (control, value) -> Internals.patchDataTemplates control.DataTemplates value) - - let compare: obj * obj -> bool = (fun (a, b) -> - let a = a :?> DataTemplates - let b = b :?> DataTemplates - System.Linq.Enumerable.SequenceEqual(a, b)) - - let factory = fun () -> DataTemplates() - - AttrBuilder<'t>.CreateProperty("DataTemplates", templates, ValueSome getter, ValueSome setter, ValueSome compare, factory) - - static member dataTemplates<'t when 't :> IDataTemplateHost>(templates: IDataTemplate list) : IAttr<'t> = - let getter: 't -> IDataTemplate list = (fun control -> control.DataTemplates |> Seq.toList) - - let setter: ('t * IDataTemplate list -> unit) = (fun (control, value) -> Internals.patchDataTemplates control.DataTemplates value) - - let factory = fun () -> [] - - AttrBuilder<'t>.CreateProperty("DataTemplates", templates, ValueSome getter, ValueSome setter, ValueNone, factory) From 7107231775827b79546c5be38deadc4ee8e8ca46 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:09:05 +0900 Subject: [PATCH 27/30] Remove onTextChanged (TextBox.TextChangingEvent -> unit) binding. --- src/Avalonia.FuncUI/DSL/TextBox.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/TextBox.fs b/src/Avalonia.FuncUI/DSL/TextBox.fs index 1e1548c6..d699036f 100644 --- a/src/Avalonia.FuncUI/DSL/TextBox.fs +++ b/src/Avalonia.FuncUI/DSL/TextBox.fs @@ -129,12 +129,10 @@ module TextBox = static member onPastingFromClipboard<'t when 't :> TextBox>(func: RoutedEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(TextBox.PastingFromClipboardEvent, func, ?subPatchOptions = subPatchOptions) + /// Create a `TextBox.TextProperty` Subscription. Not `TextBox.TextChangedEvent` Subscription. static member onTextChanged<'t when 't :> TextBox>(func: string -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(TextBox.TextProperty, func, ?subPatchOptions = subPatchOptions) - static member onTextChanged<'t when 't :> TextBox>(func: TextChangedEventArgs -> unit, ?subPatchOptions) = - AttrBuilder<'t>.CreateSubscription(TextBox.TextChangedEvent, func, ?subPatchOptions = subPatchOptions) - static member onTextChanging<'t when 't :> TextBox>(func: TextChangingEventArgs -> unit, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(TextBox.TextChangingEvent, func, ?subPatchOptions = subPatchOptions) From f75941fd01f8f0c4a6a878b5c97c5a6e20f0fe17 Mon Sep 17 00:00:00 2001 From: SilkyFowl <16532218+SilkyFowl@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:28:35 +0900 Subject: [PATCH 28/30] add test for AttrBuilder<'t>.CreateSubscription<'arg>(name, factory, func, ?subPatchOptions) --- .../VirtualDom/VirtualDom.PatcherTests.fs | 120 +++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FuncUI.UnitTests/VirtualDom/VirtualDom.PatcherTests.fs b/src/Avalonia.FuncUI.UnitTests/VirtualDom/VirtualDom.PatcherTests.fs index 400d02d3..dfde9170 100644 --- a/src/Avalonia.FuncUI.UnitTests/VirtualDom/VirtualDom.PatcherTests.fs +++ b/src/Avalonia.FuncUI.UnitTests/VirtualDom/VirtualDom.PatcherTests.fs @@ -1,16 +1,19 @@ namespace Avalonia.FuncUI.UnitTests.VirtualDom +open System +open System.Threading open System.Collections.Generic open Avalonia +open Avalonia.Controls +open Avalonia.Media open Avalonia.Styling module PatcherTests = - open Avalonia.FuncUI.VirtualDom + open Avalonia.FuncUI.Builder open Avalonia.FuncUI.DSL open Avalonia.FuncUI.Types - open Avalonia.Controls + open Avalonia.FuncUI.VirtualDom open Xunit - open Avalonia.Media [] let ``Patch Properties`` () = @@ -248,3 +251,114 @@ module PatcherTests = Assert.IsType(typeof