Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add bindings #420

Merged
merged 30 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2dce4df
fix classes and styles properties on StyledElement
SilkyFowl Apr 17, 2024
883539d
control catalog: add styles demo back
SilkyFowl Apr 17, 2024
de6c577
fix IStyleHost.styles
SilkyFowl Apr 17, 2024
b61fb25
add tests for `IStyleHost.styles` and `Control.classes` properties.
SilkyFowl Apr 17, 2024
b787bb2
add dataTemplates property.
SilkyFowl Apr 18, 2024
a5942b8
add onPropertyChanged event.
SilkyFowl Apr 18, 2024
d33a7bd
add Net Event Attr functions.
SilkyFowl Apr 18, 2024
c229623
add Visual DSL functions.
SilkyFowl Apr 18, 2024
19b68aa
use `nameof` expression
SilkyFowl Apr 18, 2024
f42c4eb
add Layoutable DSL functions.
SilkyFowl Apr 18, 2024
729ba9f
add InputElement DSL functions.
SilkyFowl Apr 18, 2024
35ee9f0
add Control DSL functions.
SilkyFowl Apr 18, 2024
023dde8
add Inline DSL functions.
SilkyFowl Apr 18, 2024
e3e4dc3
add TextDecoration DSL functions.
SilkyFowl Apr 18, 2024
305d1b8
add TextBlock DSL functions.
SilkyFowl Apr 18, 2024
540dc68
add Image DSL functions.
SilkyFowl Apr 18, 2024
4230295
move stryles DSL into StyledElement.fs
SilkyFowl Apr 19, 2024
11bd931
add Flyout DSL functions.
SilkyFowl Apr 19, 2024
7276821
refactor subscription function if passing event source, to use AddHan…
SilkyFowl Apr 19, 2024
3f00708
add TemplatedControl bindings.
SilkyFowl Apr 19, 2024
86e8af8
add TextBox bindings.
SilkyFowl Apr 19, 2024
392699b
add ItemsControl bindings.
SilkyFowl Apr 19, 2024
7d0d13f
Type parameters Modified to explicitly.
SilkyFowl Apr 19, 2024
410d228
documentation for updating `Classes`' standard classes
SilkyFowl Apr 22, 2024
92e67be
fix isPseudoClass
SilkyFowl Apr 22, 2024
1f1464b
move dataTemplates binding functions to Control.fs
SilkyFowl Apr 22, 2024
7107231
Remove onTextChanged (TextBox.TextChangingEvent -> unit) binding.
SilkyFowl Apr 22, 2024
f75941f
add test for AttrBuilder<'t>.CreateSubscription<'arg>(name, factory, …
SilkyFowl Apr 23, 2024
d05943b
Refactor list / AvaloniaList / IList value bindings
SilkyFowl Apr 23, 2024
a3226c1
fix compare function.
SilkyFowl Apr 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,10 @@ module MainView =
TabItem.header "SplitView Demo"
TabItem.content (ViewBuilder.Create<SplitViewDemo.Host>([]))
]
// ToDo: return it back when styles will be worked
//TabItem.create [
// TabItem.header "Styles Demo"
// TabItem.content (ViewBuilder.Create<StylesDemo.Host>([]))
//]
TabItem.create [
TabItem.header "Styles Demo"
TabItem.content (ViewBuilder.Create<StylesDemo.Host>([]))
]
TabItem.create [
TabItem.header "TextBox Demo"
TabItem.content (ViewBuilder.Create<TextBoxDemo.Host>([]))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="using:System">
<Styles.Resources>
<ResourceDictionary>
<!-- FluentTheme has no common FintSize Resources. -->
<sys:Double x:Key="FontSizeSmall">10</sys:Double>
<sys:Double x:Key="FontSizeNormal">12</sys:Double>
<sys:Double x:Key="FontSizeLarge">16</sys:Double>
</ResourceDictionary>
</Styles.Resources>

<Style Selector="Button.round /template/ ContentPresenter">
<Setter Property="CornerRadius" Value="10"/>
</Style>
Expand All @@ -22,9 +31,9 @@
</Style>

<Style Selector="Border.drag">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightAccentBrush}"/>
</Style>
<Style Selector="Border.drop">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightAltListAccentMediumBrush}"/>
</Style>
</Styles>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand All @@ -16,6 +16,7 @@
<Compile Include="VirtualDom\VirtualDom.ModuleTests.fs" />
<Compile Include="VirtualDom\VirtualDom.DifferTests.fs" />
<Compile Include="VirtualDom\VirtualDom.PatcherTests.fs" />
<Compile Include="DSL\Base\StyledElementTests.fs" />
<Compile Include="State.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
Expand Down
149 changes: 149 additions & 0 deletions src/Avalonia.FuncUI.UnitTests/DSL/Base/StyledElementTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
namespace Avalonia.FuncUI.UnitTests.DSL

open Avalonia
open Avalonia.Controls
open global.Xunit

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 ]

[<Fact>]
let ``classes equality with string list`` () =
let valueList() = [ "class1"; "class2" ]

let classes1 = valueList()
let classes2 = valueList()

let stringList =
(classes1, classes2)
||> twoAttrs StyledElement.classes
|> Differ.diffAttributes

Assert.Empty stringList

[<Fact>]
let ``classes equality with same classes instance`` () =
let classes = Classes()
classes.Add "class1"
classes.Add "class2"

let sameClassesInstance =
(classes, classes) ||> twoAttrs StyledElement.classes |> Differ.diffAttributes

Assert.Empty sameClassesInstance

[<Fact>]
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 StyledElement.classes |> Differ.diffAttributes

Assert.Empty differentClassesInstance

let initStyle () =
let s = Style(fun x -> x.Is<Control>())
s.Setters.Add(Setter(Control.TagProperty, "foo"))
s :> IStyle

[<Fact>]
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


[<Fact>]
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<IStyle> as [ value ]) } ->
Assert.Equal("Styles", propName)
Assert.NotEqual(style1, value)
Assert.Equal(style2, value)

| _ -> Assert.Fail $"Not expected delta\n{styleList}"

[<Fact>]
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

[<Fact>]
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

[<Fact>]
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<Styles>(styles1, value)
Assert.Equal<Styles>(styles2, value)

| _ -> Assert.Fail $"Not expected delta\n{styleList}"
120 changes: 117 additions & 3 deletions src/Avalonia.FuncUI.UnitTests/VirtualDom/VirtualDom.PatcherTests.fs
Original file line number Diff line number Diff line change
@@ -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

[<Fact>]
let ``Patch Properties`` () =
Expand Down Expand Up @@ -248,3 +251,114 @@ module PatcherTests =
Assert.IsType(typeof<Button>, stackpanel.Children.[2])
let button = stackpanel.Children.[2] :?> Button
Assert.Equal(SolidColorBrush.Parse("green").ToImmutable() :> IBrush, button.Background)

[<Fact>]
let ``Patch Custom Subscription`` () =
/// Capture list for factory called.
let factoryCaptures = ResizeArray()
/// Capture list for token cancellation called.
let tokenCancelledCaptures = ResizeArray()

/// Custom subscription binding function for testing common pattern of subscribing to .NET Event in FuncUI.
let onTextChanging (func, subPatchOptions) =
let name = "Test_TextChanged"

/// Custom subscription factory for `TextBox.TextChanging`.
let factory: AvaloniaObject * ('t * TextChangingEventArgs -> unit) * CancellationToken -> unit when 't :> TextBox =
fun (control, func, token) ->
// When factory is called, capture the subPatchOptions.
factoryCaptures.Add subPatchOptions

let control = control :?> 't
let handler = EventHandler<TextChangingEventArgs>(fun s e -> func(s :?> 't, e))
let event = control.TextChanging
event.AddHandler(handler)

// Register unsubscribe action to token.
token.Register(fun _ ->
// When token.Cancel is called, capture the subPatchOptions.
tokenCancelledCaptures.Add subPatchOptions
event.RemoveHandler(handler)
) |> ignore

AttrBuilder<'t>.CreateSubscription<'t * TextChangingEventArgs>(name, factory, func, subPatchOptions)

/// Capture list for text changing event.
let textChangingCaptures = ResizeArray()

/// testing view function for TextBox.
///
/// Control is updated by `IAttr<'t> list` in order. This test is to confirm the behavior of
/// event subscription around this specification.
let view text subPatchStr =
TextBox.create [
TextBox.text text
onTextChanging (
(fun (tb, e) -> textChangingCaptures.Add $"{subPatchStr}-{tb.Text}"),
OnChangeOf subPatchStr
)
]

/// initial view definition. Only set text value.
let initView = TextBox.create [ TextBox.text "Foo" ]
/// 1st update view definition. Add event subscription.
let updatedView = view "Foo" "FirstSubPatch"
/// 2nd update view definition. Only update text value.
let updatedView' = view "Hoge" "FirstSubPatch"
/// 3rd update view definition. Update text value and subscription subPatch.
let updatedView'' = view "Bar" "SecondSubPatch"
/// 4th update view definition. Only update text value.
let updatedView''' = view "Fuga" "SecondSubPatch"

/// create target control.
let target = VirtualDom.create initView :?> TextBox

// 1st update.
VirtualDom.update(target, initView, updatedView)
// Check text value.
Assert.Equal("Foo", target.Text)
// Check event subscription.
Assert.Equal(1, factoryCaptures.Count)
Assert.Equal(OnChangeOf "FirstSubPatch", factoryCaptures[0])
// No token cancellation.
Assert.Empty tokenCancelledCaptures
// When text has not changed, event is not fired.
Assert.Empty textChangingCaptures

// 2nd update.
VirtualDom.update(target, updatedView, updatedView')
// Text value is updated.
Assert.Equal("Hoge", target.Text)
// Subscription is not updated.
Assert.Equal(1, factoryCaptures.Count)
// No token cancellation.
Assert.Empty tokenCancelledCaptures
// Check event fired.
Assert.Equal(1, textChangingCaptures.Count)
Assert.Equal("FirstSubPatch-Hoge", textChangingCaptures[0])

// 3rd update.
VirtualDom.update(target, updatedView', updatedView'')
// Text value is updated.
Assert.Equal("Bar", target.Text)
// Subscription is updated.
Assert.Equal(2, factoryCaptures.Count)
Assert.Equal(OnChangeOf "SecondSubPatch", factoryCaptures.[1])
// Old callback is unsubscribed.
Assert.Equal(1, tokenCancelledCaptures.Count)
Assert.Equal(OnChangeOf "FirstSubPatch", tokenCancelledCaptures.[0])
// Check event fired. Text value update faster than subscription update, so old callback is called.
Assert.Equal(2, textChangingCaptures.Count)
Assert.Equal("FirstSubPatch-Bar", textChangingCaptures.[1])

// 4th update.
VirtualDom.update(target, updatedView'', updatedView''')
// Text value is updated.
Assert.Equal("Fuga", target.Text)
// Subscription is not updated.
Assert.Equal(2, factoryCaptures.Count)
// subscription is not cancelled.
Assert.Equal(1, tokenCancelledCaptures.Count)
// Check event fired. New callback is called.
Assert.Equal(3, textChangingCaptures.Count)
Assert.Equal("SecondSubPatch-Fuga", textChangingCaptures.[2])
4 changes: 3 additions & 1 deletion src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand Down Expand Up @@ -97,6 +97,8 @@
<Compile Include="DSL\Shapes\Path.fs" />
<Compile Include="DSL\Calendar\Calendar.fs" />
<Compile Include="DSL\Calendar\CalendarDatePicker.fs" />
<Compile Include="DSL\Documents\TextDecoration.fs" />
<Compile Include="DSL\Documents\Inline.fs" />
<Compile Include="DSL\Documents\Run.fs" />
<Compile Include="DSL\Documents\Span.fs" />
<Compile Include="DSL\Documents\Bold.fs" />
Expand Down
14 changes: 14 additions & 0 deletions src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
open Avalonia
open Avalonia.FuncUI
open Avalonia.FuncUI.Types
open System.Threading

[<AutoOpen>]
module AvaloniaObject =
open Avalonia.FuncUI.Types
open Avalonia.FuncUI.Builder

type AvaloniaObject with

Expand All @@ -31,9 +34,20 @@
InitFunction.Function = (fun (control: obj) -> func (control :?> 't))
}

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
let disposable = control.PropertyChanged.Subscribe(func)

token.Register(fun () -> disposable.Dispose()) |> ignore)

AttrBuilder<'t>.CreateSubscription<AvaloniaPropertyChangedEventArgs>(name, factory, func, ?subPatchOptions = subPatchOptions)

member this.Bind(prop: DirectPropertyBase<'value>, readable: #IReadable<'value>) : unit =
let _ = this.Bind(property = prop, source = readable.ImmediateObservable)

Check warning on line 49 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.

Check warning on line 49 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.

Check warning on line 49 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.
()

member this.Bind(prop: StyledProperty<'value>, readable: #IReadable<'value>) : unit =
let _ = this.Bind(property = prop, source = readable.ImmediateObservable)

Check warning on line 53 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.

Check warning on line 53 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.

Check warning on line 53 in src/Avalonia.FuncUI/DSL/Base/AvaloniaObject.fs

View workflow job for this annotation

GitHub Actions / build

Same as Observable, but fires once immediately after subscribing. This warning can be disabled using '--nowarn:57' or '#nowarn "57"'.
Expand Down
Loading
Loading