]
41 | static member inline Fatal(this: Logger, ex: exn) =
42 | LoggerExtensions.Log(this, LogLevel.Fatal, "{0}", ex.ToString())
43 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/win-i5-9600kf-2021-12-21/Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-report.csv:
--------------------------------------------------------------------------------
1 | Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,depth,Mean,Error,StdDev,Gen 0,Gen 1,Gen 2,Allocated
2 | CreateWidgets,.NET 6.0,False,Default,Default,Default,Default,Default,Default,111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10,323.7 μs,1.85 μs,1.54 μs,106.4453,38.5742,-,551 KB
3 | CreateWidgets,.NET 6.0,False,Default,Default,Default,Default,Default,Default,111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,20,"101,939.1 μs","1,878.27 μs","1,756.94 μs",11600.0000,2800.0000,800.0000,"68,072 KB"
4 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/macos-i7-6820hq-2021-12-21/Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-report.csv:
--------------------------------------------------------------------------------
1 | Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,depth,Mean,Error,StdDev,Gen 0,Gen 1,Gen 2,Allocated
2 | CreateWidgets,.NET 6.0,False,Default,Default,Default,Default,Default,Default,00000000,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10,439.6 μs,3.39 μs,3.00 μs,120.6055,45.4102,-,551 KB
3 | CreateWidgets,.NET 6.0,False,Default,Default,Default,Default,Default,Default,00000000,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,20,"159,309.5 μs","3,076.23 μs","3,159.06 μs",14000.0000,3000.0000,750.0000,"68,072 KB"
4 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/win-i5-9600kf-2021-12-21/Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-report.csv:
--------------------------------------------------------------------------------
1 | Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,depth,Mean,Error,StdDev,Gen 0,Gen 1,Gen 2,Allocated
2 | ProcessMessages,.NET 6.0,False,Default,Default,Default,Default,Default,Default,111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10,173.7 ms,2.13 ms,1.89 ms,40000.0000,5000.0000,2000.0000,222 MB
3 | ProcessMessages,.NET 6.0,False,Default,Default,Default,Default,Default,Default,111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,15,"4,920.9 ms",63.42 ms,59.32 ms,510000.0000,196000.0000,96000.0000,"2,470 MB"
4 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/macos-i7-6820hq-2021-12-21/Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-report.csv:
--------------------------------------------------------------------------------
1 | Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,depth,Mean,Error,StdDev,Gen 0,Gen 1,Gen 2,Allocated
2 | ProcessMessages,.NET 6.0,False,Default,Default,Default,Default,Default,Default,00000000,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10,257.5 ms,3.79 ms,3.36 ms,46000.0000,14000.0000,2000.0000,222 MB
3 | ProcessMessages,.NET 6.0,False,Default,Default,Default,Default,Default,Default,00000000,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 6.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,15,"7,457.8 ms",33.21 ms,31.06 ms,605000.0000,196000.0000,96000.0000,"2,470 MB"
4 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/win-i5-9600kf-2021-12-21/Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-20211221-143350
6 |
7 |
13 |
14 |
15 |
16 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
17 | Intel Core i5-9600KF CPU 3.70GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
18 | .NET SDK=6.0.101
19 | [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT DEBUG
20 | .NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
21 |
22 | Job=.NET 6.0 Runtime=.NET 6.0
23 |
24 |
25 |
26 | | Method | depth | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
27 |
28 | | ProcessMessages | 10 | 173.7 ms | 2.13 ms | 1.89 ms | 40000.0000 | 5000.0000 | 2000.0000 | 222 MB |
29 |
| ProcessMessages | 15 | 4,920.9 ms | 63.42 ms | 59.32 ms | 510000.0000 | 196000.0000 | 96000.0000 | 2,470 MB |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/win-i5-9600kf-2021-12-21/Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-20211221-143315
6 |
7 |
13 |
14 |
15 |
16 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
17 | Intel Core i5-9600KF CPU 3.70GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
18 | .NET SDK=6.0.101
19 | [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT DEBUG
20 | .NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
21 |
22 | Job=.NET 6.0 Runtime=.NET 6.0
23 |
24 |
25 |
26 | | Method | depth | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
27 |
28 | | CreateWidgets | 10 | 323.7 μs | 1.85 μs | 1.54 μs | 106.4453 | 38.5742 | - | 551 KB |
29 |
| CreateWidgets | 20 | 101,939.1 μs | 1,878.27 μs | 1,756.94 μs | 11600.0000 | 2800.0000 | 800.0000 | 68,072 KB |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/macos-i7-6820hq-2021-12-21/Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fabulous.Tests.Benchmarks.DiffingAttributes.Benchmarks-20211221-145122
6 |
7 |
13 |
14 |
15 |
16 | BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.1 (21C52) [Darwin 21.2.0]
17 | Intel Core i7-6820HQ CPU 2.70GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
18 | .NET SDK=6.0.101
19 | [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT DEBUG
20 | .NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
21 |
22 | Job=.NET 6.0 Runtime=.NET 6.0
23 |
24 |
25 |
26 | | Method | depth | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
27 |
28 | | ProcessMessages | 10 | 257.5 ms | 3.79 ms | 3.36 ms | 46000.0000 | 14000.0000 | 2000.0000 | 222 MB |
29 |
| ProcessMessages | 15 | 7,457.8 ms | 33.21 ms | 31.06 ms | 605000.0000 | 196000.0000 | 96000.0000 | 2,470 MB |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/snapshots/macos-i7-6820hq-2021-12-21/Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fabulous.Tests.Benchmarks.NestedTreeCreation.Benchmarks-20211221-145034
6 |
7 |
13 |
14 |
15 |
16 | BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.1 (21C52) [Darwin 21.2.0]
17 | Intel Core i7-6820HQ CPU 2.70GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
18 | .NET SDK=6.0.101
19 | [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT DEBUG
20 | .NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
21 |
22 | Job=.NET 6.0 Runtime=.NET 6.0
23 |
24 |
25 |
26 | | Method | depth | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
27 |
28 | | CreateWidgets | 10 | 439.6 μs | 3.39 μs | 3.00 μs | 120.6055 | 45.4102 | - | 551 KB |
29 |
| CreateWidgets | 20 | 159,309.5 μs | 3,076.23 μs | 3,159.06 μs | 14000.0000 | 3000.0000 | 750.0000 | 68,072 KB |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Fabulous.Tests/APISketchTests/TestUI.ViewUpdaters.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous.Tests.APISketchTests
2 |
3 | module TestUI_ViewUpdaters =
4 |
5 | open Fabulous
6 | open Platform
7 |
8 | let updateText _ (newValueOpt: string voption) (node: IViewNode) =
9 | let textElement = node.Target :?> IText
10 | textElement.Text <- ValueOption.defaultValue "" newValueOpt
11 |
12 | let updateRecord _ (newValueOpt: bool voption) (node: IViewNode) =
13 | let textElement = node.Target :?> TestLabel
14 | textElement.record <- ValueOption.defaultValue false newValueOpt
15 |
16 | let updateTextColor _ (newValueOpt: string voption) (node: IViewNode) =
17 | let textElement = node.Target :?> IText
18 | textElement.TextColor <- ValueOption.defaultValue "" newValueOpt
19 |
20 | let updateAutomationId _ (newValueOpt: string voption) (node: IViewNode) =
21 | let el = node.Target :?> TestViewElement
22 | el.AutomationId <- ValueOption.defaultValue "" newValueOpt
23 |
24 |
25 | let updateNumericValueOne _ (newValueOpt: uint64 voption) (node: IViewNode) =
26 | let el = node.Target :?> TestNumericBag
27 | el.valueOne <- ValueOption.defaultValue 0UL newValueOpt
28 |
29 | let updateNumericValueTwo _ (newValueOpt: uint64 voption) (node: IViewNode) =
30 | let el = node.Target :?> TestNumericBag
31 | el.valueTwo <- ValueOption.defaultValue 0UL newValueOpt
32 |
33 | let updateNumericValueThree _ (newValueOpt: float voption) (node: IViewNode) =
34 | let el = node.Target :?> TestNumericBag
35 | el.valueThree <- ValueOption.defaultValue 0. newValueOpt
36 |
--------------------------------------------------------------------------------
/src/Fabulous.Tests/Fabulous.Tests.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 | Library
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | all
29 | runtime; build; native; contentfiles; analyzers; buildtransitive
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/Fabulous.Tests/AttributesTests.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous.Tests
2 |
3 | open Fabulous.Tests
4 | open NUnit.Framework
5 | open FsCheck.NUnit
6 | open Fabulous
7 |
8 | []
9 | type AttributesTests() =
10 | []
11 | member _.``Encoding then decoding a bool should return an identical bool``(value: bool) =
12 | let encoded = SmallScalars.Bool.encode value
13 | let decoded = SmallScalars.Bool.decode encoded
14 | Assert.AreEqual(value, decoded)
15 |
16 | []
17 | member _.``Encoding then decoding a float should return an identical float``(value: float) =
18 | let encoded = SmallScalars.Float.encode value
19 | let decoded = SmallScalars.Float.decode encoded
20 | Assert.AreEqual(value, decoded)
21 |
22 | []
23 | member _.``Encoding then decoding a float32 should return an identical float32``(value: float32) =
24 | let encoded = SmallScalars.Float32.encode value
25 | let decoded = SmallScalars.Float32.decode encoded
26 | Assert.AreEqual(value, decoded)
27 |
28 | []
29 | member _.``Encoding then decoding an int should return an identical int``(value: int) =
30 | let encoded = SmallScalars.Int.encode value
31 | let decoded = SmallScalars.Int.decode encoded
32 | Assert.AreEqual(value, decoded)
33 |
34 | []
35 | member _.``Encoding then decoding an int-typed enum should return an identical int-typed enum``(value: IntTypedEnum) =
36 | let encoded = SmallScalars.IntEnum.encode value
37 |
38 | let decoded = SmallScalars.IntEnum.decode encoded
39 |
40 | Assert.AreEqual(value, decoded)
41 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches: [ 'main' ]
5 | paths-ignore: [ 'docs/**' ]
6 |
7 | permissions: write-all
8 |
9 | env:
10 | CONFIG: Release
11 | SLN_FILE: Fabulous.sln
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout sources
18 | uses: actions/checkout@v3
19 | - name: Read last version from CHANGELOG.md
20 | id: changelog_reader
21 | uses: mindsers/changelog-reader-action@v2
22 | with:
23 | validation_level: warn
24 | path: ./CHANGELOG.md
25 | - name: Set nightly version
26 | run: |
27 | NIGHTLY_VERSION=${{ steps.changelog_reader.outputs.version }}-nightly-${GITHUB_RUN_ID}
28 | echo "Nightly version is $NIGHTLY_VERSION"
29 | echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> "$GITHUB_ENV"
30 | - name: Setup .NET
31 | uses: actions/setup-dotnet@v3
32 | with:
33 | dotnet-version: 9.x
34 | - name: Restore
35 | run: dotnet restore ${SLN_FILE}
36 | - name: Build
37 | run: dotnet build ${SLN_FILE} -p:Version=${NIGHTLY_VERSION} -c ${CONFIG} --no-restore
38 | - name: Test
39 | run: dotnet test ${SLN_FILE} -p:Version=${NIGHTLY_VERSION} -c ${CONFIG} --no-build
40 | - name: Pack
41 | run: dotnet pack ${SLN_FILE} -p:Version=${NIGHTLY_VERSION} -c ${CONFIG} --no-build --property PackageOutputPath=${PWD}/nupkgs
42 | - name: Upload artifacts
43 | uses: actions/upload-artifact@v4
44 | with:
45 | name: Packages
46 | path: nupkgs/
47 | - name: Push
48 | run: dotnet nuget push "nupkgs/*" -s https://nuget.pkg.github.com/fabulous-dev/index.json -k ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
49 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Environment.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System.Runtime.CompilerServices
4 | open Fabulous.ScalarAttributeDefinitions
5 |
6 | type EnvironmentRequest<'T> = delegate of unit -> EnvironmentKey<'T>
7 |
8 | []
9 | module EnvironmentBuilders =
10 | type Context with
11 | static member inline Environment(key: EnvironmentKey<'T>) = EnvironmentRequest(fun () -> key)
12 |
13 | type EnvironmentAttrValue = { Key: string; Value: obj }
14 |
15 | []
16 | type EnvironmentExtensions =
17 | []
18 | static member inline Bind
19 | (
20 | _: ComponentBuilder<'parentMsg, 'marker>,
21 | [] value: EnvironmentRequest<'T>,
22 | [] continuation: 'T -> ComponentBodyBuilder<'msg, 'marker>
23 | ) =
24 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
25 | let envKey = value.Invoke()
26 | let (EnvironmentAttributeKey key) = envKey.Key
27 |
28 | // Listen to changes in the environment
29 | context.LinkDisposable(
30 | $"env_{key}",
31 | fun () ->
32 | envContext.ValueChanged.Subscribe(fun args ->
33 | if args.Key = envKey.Key then
34 | context.NeedsRender())
35 | )
36 | |> ignore
37 |
38 | let state = envContext.Get(envKey)
39 | (continuation state).Invoke(envContext, treeContext, context, bindings))
40 |
41 | []
42 | type EnvironmentModifiers =
43 | []
44 | static member inline environment(this: WidgetBuilder<'msg, 'marker>, key: EnvironmentKey<'T>, value: 'T) = this.AddEnvironment(key.Key, value)
45 |
--------------------------------------------------------------------------------
/src/Fabulous/ViewRef.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 |
5 | /// A reference to be able to access the underlying control of a Widget
6 | type ViewRef<'T when 'T: not struct>() as this =
7 | let attached = Event, 'T>()
8 | let detached = Event()
9 |
10 | let onAttached target = attached.Trigger(this, unbox target)
11 | let onDetached () = detached.Trigger(this, EventArgs())
12 |
13 | let handle = ViewRef(onAttached, onDetached)
14 |
15 | /// Event triggered when the view is attached
16 | []
17 | member _.Attached = attached.Publish
18 |
19 | /// Event triggered when the view is detached
20 | []
21 | member _.Detached = detached.Publish
22 |
23 | /// The underlying control.
24 | /// This property might throw an exception if the control is not attached to the ViewRef
25 | member _.Value: 'T =
26 | match handle.TryValue with
27 | | Some res -> unbox res
28 | | None -> failwith "view reference target has been collected or was not set"
29 |
30 | /// The underlying control. If the control is not attached to the ViewRef, None will be returned
31 | member _.TryValue: 'T option =
32 | match handle.TryValue with
33 | | Some res -> Some(unbox res)
34 | | None -> None
35 |
36 | /// The untyped ViewRef
37 | member _.Unbox = handle
38 |
39 | module ViewRefAttributes =
40 | let ViewRef =
41 | Attributes.defineSimpleScalarWithEquality "Fabulous_ViewRef" (fun oldValueOpt newValueOpt node ->
42 | match oldValueOpt with
43 | | ValueNone -> ()
44 | | ValueSome viewRef -> viewRef.Unset()
45 |
46 | match newValueOpt with
47 | | ValueNone -> ()
48 | | ValueSome viewRef -> viewRef.Set(node.Target))
49 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags: ['[0-9]+.[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+-pre[0-9]+']
5 | paths-ignore: [ 'docs/**' ]
6 |
7 | permissions: write-all
8 |
9 | env:
10 | CONFIG: Release
11 | SLN_FILE: Fabulous.sln
12 |
13 | jobs:
14 | release:
15 | runs-on: ubuntu-latest
16 | environment: nuget
17 | steps:
18 | - name: Checkout sources
19 | uses: actions/checkout@v3
20 | - name: Extract version from tag
21 | uses: damienaicheh/extract-version-from-tag-action@v1.0.0
22 | - name: Set release version
23 | run: |
24 | if [ "${PRE_RELEASE}" == "" ]; then
25 | RELEASE_VERSION=${MAJOR}.${MINOR}.${PATCH}
26 | else
27 | RELEASE_VERSION=${MAJOR}.${MINOR}.${PATCH}-${PRE_RELEASE}
28 | fi
29 | echo "Release version is $RELEASE_VERSION"
30 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV"
31 | - name: Get Changelog Entry
32 | id: changelog_reader
33 | uses: mindsers/changelog-reader-action@v2
34 | with:
35 | validation_level: warn
36 | version: '${{ env.RELEASE_VERSION }}'
37 | path: ./CHANGELOG.md
38 | - name: Setup .NET
39 | uses: actions/setup-dotnet@v3
40 | with:
41 | dotnet-version: 9.x
42 | - name: Restore
43 | run: dotnet restore ${SLN_FILE}
44 | - name: Build
45 | run: dotnet build ${SLN_FILE} -p:Version=${RELEASE_VERSION} -c ${CONFIG} --no-restore
46 | - name: Test
47 | run: dotnet test ${SLN_FILE} -p:Version=${RELEASE_VERSION} -c ${CONFIG} --no-build
48 | - name: Pack
49 | run: dotnet pack ${SLN_FILE} -p:Version=${RELEASE_VERSION} -p:PackageReleaseNotes="${{ steps.changelog_reader.outputs.changes }}" -c ${CONFIG} --no-build --property PackageOutputPath=${PWD}/nupkgs
50 | - name: Upload artifacts
51 | uses: actions/upload-artifact@v4
52 | with:
53 | name: Packages
54 | path: nupkgs/
55 | - name: Push
56 | run: dotnet nuget push "nupkgs/*" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_TOKEN }} --skip-duplicate
--------------------------------------------------------------------------------
/src/Fabulous/Components/EnvironmentObject.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.Runtime.CompilerServices
5 |
6 | type EnvironmentObjectRequest<'T> = delegate of unit -> EnvironmentKey<'T>
7 |
8 | []
9 | type EnvironmentObject() =
10 | let changed = Event()
11 | member this.Changed = changed.Publish
12 | member this.NotifyChanged() = changed.Trigger()
13 | abstract member Dispose: unit -> unit
14 | default this.Dispose() = ()
15 |
16 | interface IDisposable with
17 | member this.Dispose() = this.Dispose()
18 |
19 | []
20 | module EnvironmentObjectBuilders =
21 | type Context with
22 | static member inline EnvironmentObject<'T when 'T :> EnvironmentObject>(key: EnvironmentKey<'T>) = EnvironmentObjectRequest(fun () -> key)
23 |
24 | []
25 | type EnvironmentObjectExtensions =
26 | []
27 | static member inline Bind<'parentMsg, 'marker, 'msg, 'T when 'parentMsg: equality and 'msg: equality and 'T :> EnvironmentObject>
28 | (
29 | _: ComponentBuilder<'parentMsg, 'marker>,
30 | [] value: EnvironmentObjectRequest<'T>,
31 | [] continuation: 'T -> ComponentBodyBuilder<'msg, 'marker>
32 | ) =
33 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
34 | let envKey = value.Invoke()
35 | let (EnvironmentAttributeKey key) = envKey.Key
36 |
37 | // Listen to changes in the environment
38 | context.LinkDisposable(
39 | $"env_{key}",
40 | fun () ->
41 | envContext.ValueChanged.Subscribe(fun args ->
42 | if args.Key = envKey.Key then
43 | context.NeedsRender())
44 | )
45 | |> ignore
46 |
47 | let state = envContext.Get(envKey)
48 |
49 | // Listen to changes in the object
50 | context.LinkDisposable($"env_{key}_sub", fun () -> state.Changed.Subscribe(fun () -> context.NeedsRender()))
51 | |> ignore
52 |
53 | (continuation state).Invoke(envContext, treeContext, context, bindings))
54 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Binding.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.ComponentModel
5 | open System.Runtime.CompilerServices
6 |
7 | type BindingRequest<'T> = delegate of unit -> StateValue<'T>
8 |
9 | []
10 | type BindingValue<'T> =
11 | []
12 | val public SourceContext: ComponentContext
13 |
14 | []
15 | val public SourceKey: int
16 |
17 | new(stateValue: StateValue<'T>) =
18 | { SourceContext = stateValue.Context
19 | SourceKey = stateValue.Key }
20 |
21 | member this.Current = this.SourceContext.TryGetValue<'T>(this.SourceKey).Value
22 |
23 | member inline this.Set(value: 'T) =
24 | this.SourceContext.SetValue(this.SourceKey, value)
25 |
26 | []
27 | module BindingBuilders =
28 | type Context with
29 |
30 | static member inline Binding(value: StateValue<'T>) = BindingRequest<'T>(fun () -> value)
31 |
32 | []
33 | type BindingExtensions =
34 | []
35 | static member inline Bind
36 | (
37 | _: ComponentBuilder<'parentMsg, 'marker>,
38 | [] fn: BindingRequest<'T>,
39 | [] continuation: BindingValue<'T> -> ComponentBodyBuilder<'msg, 'marker>
40 | ) =
41 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
42 | let key = int bindings
43 | let stateValue = fn.Invoke()
44 |
45 | // Dispose previous subscription
46 | match context.TryGetValue(key) with
47 | | ValueNone -> ()
48 | | ValueSome d -> d.Dispose()
49 |
50 | // Subscribe to source context changes
51 | let sourceKey = stateValue.Key
52 |
53 | //let sub =
54 | // stateValue.Context.RenderNeeded.Subscribe(fun k ->
55 | // if k = sourceKey then
56 | // ctx.NeedsRender(key))
57 | //
58 | //ctx.SetValueInternal(key, sub)
59 |
60 | let bindingValue = BindingValue<'T>(stateValue)
61 |
62 | (continuation bindingValue).Invoke(envContext, treeContext, context, bindings + 1))
63 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Mvu.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System.Runtime.CompilerServices
4 |
5 | type ModelValue<'model> = private ModelValue of 'model
6 |
7 | type MvuRequest<'arg, 'model, 'msg> = delegate of unit -> struct (Program<'arg, 'model, 'msg> * 'arg)
8 |
9 | []
10 | module MvuBuilders =
11 | type Context with
12 | static member inline Mvu(program: Program) =
13 | MvuRequest(fun () -> program, ())
14 |
15 | static member inline Mvu(program: Program<'arg, 'model, 'msg>, arg: 'arg) =
16 | MvuRequest<'arg, 'model, 'msg>(fun () -> program, arg)
17 |
18 | []
19 | type MvuExtensions =
20 | []
21 | static member Bind
22 | (_: ComponentBuilder<'parentMsg, 'marker>, fn: MvuRequest<'arg, 'model, 'msg>, continuation: 'model -> ComponentBodyBuilder<'msg, 'marker>)
23 | =
24 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
25 | let key = int bindings
26 |
27 | let struct (treeContext, state) =
28 | match context.TryGetValue(key) with
29 | | ValueSome(ModelValue state) -> treeContext, state
30 | | ValueNone ->
31 | let struct (program, arg) = fn.Invoke()
32 |
33 | let runner =
34 | context.LinkDisposable(
35 | "runner",
36 | fun () ->
37 | let getModel () =
38 | match context.TryGetValue>(key) with
39 | | ValueNone -> failwith("Model not found in ComponentContext " + context.Id.ToString())
40 | | ValueSome(ModelValue model) -> model
41 |
42 | let setModel v = context.SetValue(key, ModelValue v)
43 |
44 | new Runner<'arg, 'model, 'msg>(getModel, setModel, program)
45 | )
46 |
47 | // Redirect messages to runner
48 | let treeContext =
49 | { treeContext with
50 | Dispatch = unbox >> runner.Dispatch }
51 |
52 | runner.Start(arg)
53 |
54 | let (ModelValue state) = context.TryGetValue(key).Value
55 |
56 | treeContext, state
57 |
58 | (continuation state).Invoke(envContext, treeContext, context, bindings + 1))
59 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Builder.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | /// Delegate used by the ComponentBuilder to compose a component body
4 | /// It will be aggressively inlined by the compiler leaving no overhead, only a pure function that returns a WidgetBuilder
5 | type ComponentBodyBuilder<'msg, 'marker when 'msg: equality> =
6 | delegate of
7 | envContext: EnvironmentContext * treeContext: ViewTreeContext * context: ComponentContext * bindings: int ->
8 | struct (EnvironmentContext * ViewTreeContext * ComponentContext * int * WidgetBuilder<'msg, 'marker>)
9 |
10 | type ComponentBuilder<'parentMsg, 'marker when 'parentMsg: equality> =
11 | val public Key: string
12 |
13 | new(key: string) = { Key = key }
14 |
15 | member inline this.Yield(widgetBuilder: WidgetBuilder<'msg, 'marker>) =
16 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings -> struct (envContext, treeContext, context, bindings, widgetBuilder))
17 |
18 | member inline this.Combine([] a: ComponentBodyBuilder<'msg, 'marker>, [] b: ComponentBodyBuilder<'msg, 'marker>) =
19 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
20 | let struct (envA, treeA, ctxA, bindingsA, _) =
21 | a.Invoke(envContext, treeContext, context, bindings) // discard the previous widget in the chain but we still need to count the bindings
22 |
23 | let struct (envB, treeB, ctxB, bindingsB, widgetB) =
24 | b.Invoke(envA, treeA, ctxA, bindingsA)
25 |
26 | // Calculate the total number of bindings between A and B
27 | let combinedBindings = (bindingsA + bindingsB) - bindings
28 |
29 | struct (envB, treeB, ctxB, combinedBindings, widgetB))
30 |
31 | member inline this.Delay([] fn: unit -> ComponentBodyBuilder<'msg, 'marker>) =
32 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
33 | let sub = fn()
34 | sub.Invoke(envContext, treeContext, context, bindings))
35 |
36 | member inline this.Run([] body: ComponentBodyBuilder<'msg, 'marker>) =
37 | let compiledBody =
38 | ComponentBody(fun envContext treeContext context ->
39 | let struct (envA, treeA, ctxA, _, result) =
40 | body.Invoke(envContext, treeContext, context, 0)
41 |
42 | struct (envA, treeA, ctxA, result.Compile()))
43 |
44 | let data = { Key = this.Key; Body = compiledBody }
45 |
46 | WidgetBuilder<'parentMsg, 'marker>(Component'.WidgetKey, Component'.Data.WithValue(data))
47 |
--------------------------------------------------------------------------------
/src/Fabulous/IViewNode.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open Fabulous
5 |
6 | type ViewRef(onAttached, onDetached) =
7 | let handle = System.WeakReference(null)
8 |
9 | /// Check if the new target is the same than the previous one
10 | /// This is done to avoid triggering change events when nothing changes
11 | member private _.IsSameTarget(target) =
12 | match handle.TryGetTarget() with
13 | | true, res when res = target -> true
14 | | _ -> false
15 |
16 | member x.Set(target: obj) : unit =
17 | if not(x.IsSameTarget(target)) then
18 | handle.SetTarget(target)
19 | onAttached target
20 |
21 | member x.Unset() : unit =
22 | if not(x.IsSameTarget(null)) then
23 | handle.SetTarget(null)
24 | onDetached()
25 |
26 | member _.TryValue =
27 | match handle.TryGetTarget() with
28 | | true, null -> None
29 | | true, res -> Some res
30 | | _ -> None
31 |
32 | /// Context of the whole view tree
33 | []
34 | type ViewTreeContext =
35 | { CanReuseView: Widget -> Widget -> bool
36 | GetViewNode: obj -> IViewNode
37 | Logger: Logger
38 | Dispatch: obj -> unit
39 | SyncAction: (unit -> unit) -> unit
40 | GetComponent: obj -> obj
41 | SetComponent: obj -> obj -> unit }
42 |
43 | and IViewNode =
44 | inherit IDisposable
45 |
46 | /// The view that is being rendered
47 | abstract member Target: obj
48 |
49 | /// The context of the whole view tree
50 | abstract member TreeContext: ViewTreeContext
51 |
52 | /// The environment context
53 | abstract member EnvironmentContext: EnvironmentContext
54 |
55 | // note that Widget is struct type, thus we have boxing via option
56 | // we don't have MemoizedWidget set for 99.9% of the cases
57 | // thus makes sense to have overhead of boxing
58 | // in order to save space
59 | abstract member MemoizedWidget: Widget option with get, set
60 |
61 | /// The parent node
62 | abstract member Parent: IViewNode option
63 |
64 | /// Indicate if the node has been disconnected from the tree
65 | abstract member IsDisconnected: bool
66 |
67 | /// Convert the node messages to its parent's message type
68 | abstract member MapMsg: (obj -> obj) option with get, set
69 |
70 | /// Return the event handler for a given attribute key if set
71 | abstract member TryGetHandler: string -> IDisposable voption
72 |
73 | /// Set the event handler for a given attribute name
74 | abstract member SetHandler: string * IDisposable -> unit
75 |
76 | abstract member RemoveHandler: string -> unit
77 |
78 | /// Apply the diffing result to this node
79 | abstract member ApplyDiff: WidgetDiff inref -> unit
80 |
--------------------------------------------------------------------------------
/src/Fabulous/View.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | module ViewHelpers =
4 | let canReuseView (prevWidget: Widget) (currWidget: Widget) =
5 | let prevKey = prevWidget.Key
6 |
7 | if not(prevKey = currWidget.Key) then
8 | false
9 | else if (prevKey = Memo.MemoWidgetKey) then
10 | Memo.canReuseMemoizedWidget prevWidget currWidget
11 | else if (prevKey = Component'.WidgetKey) then
12 | Component'.canReuseComponent prevWidget currWidget
13 | else
14 | true
15 |
16 | module View =
17 | /// Avoid recomputing the whole subtree when the key doesn't change
18 | let lazy'<'msg, 'key, 'marker when 'msg: equality and 'key: equality>
19 | (fn: 'key -> WidgetBuilder<'msg, 'marker>)
20 | (key: 'key)
21 | : WidgetBuilder<'msg, Memo.Memoized<'marker>> =
22 |
23 | let memo: Memo.MemoData =
24 | { KeyData = box key
25 | KeyComparer = fun (prev: obj) (next: obj) -> unbox<'key> prev = unbox<'key> next
26 | CreateWidget = fun k -> fn(unbox<'key> k).Compile()
27 | KeyType = typeof<'key>
28 | MarkerType = typeof<'marker> }
29 |
30 | WidgetBuilder<'msg, Memo.Memoized<'marker>>(Memo.MemoWidgetKey, Memo.MemoAttribute.WithValue(memo))
31 |
32 | /// Map the widget's message type to the parent's message type to allow for view composition
33 | let inline map ([] fn: 'oldMsg -> 'newMsg) (x: WidgetBuilder<'oldMsg, 'marker>) : WidgetBuilder<'newMsg, 'marker> =
34 | let replaceWith (oldAttr: ScalarAttribute) =
35 | let fnWithBoxing (msg: obj) =
36 | let oldFn = unbox obj> oldAttr.Value
37 |
38 | if not(isNull msg) && typeof<'newMsg>.IsAssignableFrom(msg.GetType()) then
39 | box msg
40 | else
41 | oldFn msg |> unbox<'oldMsg> |> fn |> box
42 |
43 | { oldAttr with Value = fnWithBoxing }
44 |
45 | let defaultWith () =
46 | let mappedFn (msg: obj) =
47 | if not(isNull msg) && typeof<'newMsg>.IsAssignableFrom(msg.GetType()) then
48 | box msg
49 | else
50 | unbox<'oldMsg> msg |> fn |> box
51 |
52 | MapMsg.MapMsg.WithValue(mappedFn)
53 |
54 | let builder = x.AddOrReplaceScalar(MapMsg.MapMsg.Key, replaceWith, defaultWith)
55 |
56 | WidgetBuilder<'newMsg, 'marker>(builder.Key, builder.Attributes)
57 |
58 | /// Combine map and lazy. Map the widget's message type to the parent's message type, and then memoize it
59 | let inline lazyMap ([] mapFn: 'oldMsg -> 'newMsg) ([] viewFn: 'key -> WidgetBuilder<'oldMsg, 'marker>) (model: 'key) =
60 | lazy' (viewFn >> map mapFn) model
61 |
--------------------------------------------------------------------------------
/src/Fabulous/Dispatch.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open Fabulous.ScalarAttributeDefinitions
4 |
5 | module Dispatcher =
6 | let private getCanDispatchAndMapMsg (node: IViewNode) : struct (bool * (obj -> obj)) =
7 | let mutable canDispatch = true
8 | let mutable currentNode = Some node
9 | let mutable mapMsg = id
10 |
11 | while currentNode.IsSome && canDispatch do
12 | mapMsg <-
13 | match currentNode.Value.MapMsg with
14 | | None -> mapMsg
15 | | Some fn -> mapMsg >> fn
16 |
17 | // Disable dispatch if the node has been disconnected from the tree
18 | if currentNode.Value.IsDisconnected then
19 | canDispatch <- false
20 |
21 | currentNode <- currentNode.Value.Parent
22 |
23 | struct (canDispatch, mapMsg)
24 |
25 | /// Dispatch a msg for the current ViewNode
26 | let dispatch (node: IViewNode) msg =
27 | let struct (canDispatch, mapMsg) = getCanDispatchAndMapMsg node
28 |
29 | if canDispatch then
30 | node.TreeContext.Dispatch(mapMsg msg)
31 |
32 | /// Trigger an event for the node and all its descendants declaring the given event definition
33 | let dispatchEventForAllChildren (node: IViewNode) (rootWidget: Widget) (definition: SimpleScalarAttributeDefinition) =
34 | let rec dispatchAndVisitChildren skipMapMsg dispatch widget =
35 | // Check if the current widget has a MapMsg function and apply it before dispatch
36 | let dispatch =
37 | if skipMapMsg then
38 | dispatch
39 | else
40 | match AttributeHelpers.tryFindSimpleScalarAttribute MapMsg.MapMsg widget with
41 | | ValueNone -> dispatch
42 | | ValueSome fn -> fn >> dispatch
43 |
44 | match AttributeHelpers.tryFindSimpleScalarAttribute definition widget with
45 | | ValueNone -> ()
46 | | ValueSome msg -> dispatch msg
47 |
48 | match widget.WidgetAttributes with
49 | | [||] -> ()
50 | | widgetAttrs ->
51 | for childAttr in widgetAttrs do
52 | dispatchAndVisitChildren false dispatch childAttr.Value
53 |
54 | match widget.WidgetCollectionAttributes with
55 | | [||] -> ()
56 | | widgetCollAttrs ->
57 | for widgetCollAttr in widgetCollAttrs do
58 | for childWidget in ArraySlice.toSpan widgetCollAttr.Value do
59 | dispatchAndVisitChildren false dispatch childWidget
60 |
61 | let struct (canDispatch, mapMsg) = getCanDispatchAndMapMsg node
62 |
63 | if canDispatch then
64 | dispatchAndVisitChildren true (mapMsg >> node.TreeContext.Dispatch) rootWidget
65 |
--------------------------------------------------------------------------------
/src/Fabulous/Fabulous.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0;netstandard2.1
4 | Fabulous
5 | true
6 |
7 |
8 |
9 | Declarative UI framework with F# and MVU
10 | true
11 | en-US
12 |
13 |
14 |
15 | true
16 | true
17 | true
18 |
19 |
20 | true
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Fabulous.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A6931CB-FA8E-4519-8EFA-61BF7E81E301}"
7 | EndProject
8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fabulous", "src\Fabulous\Fabulous.fsproj", "{F3C378FD-2A04-42FA-9B89-4F691B92CE0D}"
9 | EndProject
10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fabulous.Benchmarks", "src\Fabulous.Benchmarks\Fabulous.Benchmarks.fsproj", "{3CA02AD8-6BD9-4E8A-857F-BCF800B12247}"
11 | EndProject
12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fabulous.Tests", "src\Fabulous.Tests\Fabulous.Tests.fsproj", "{31A18443-E05B-47F1-812E-4FEECCCA06DB}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Files", "_Solution Files", "{20E44B0C-FA62-4DE4-8017-95AAE3B178CE}"
15 | ProjectSection(SolutionItems) = preProject
16 | README.md = README.md
17 | CHANGELOG.md = CHANGELOG.md
18 | LICENSE.md = LICENSE.md
19 | .gitignore = .gitignore
20 | pull_request.yml = .github/workflows/pull_request.yml
21 | build.yml = .github/workflows/build.yml
22 | release.yml = .github/workflows/release.yml
23 | .editorconfig = .editorconfig
24 | Directory.Packages.props = Directory.Packages.props
25 | Directory.Build.props = Directory.Build.props
26 | EndProjectSection
27 | EndProject
28 | Global
29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
30 | Debug|Any CPU = Debug|Any CPU
31 | Release|Any CPU = Release|Any CPU
32 | EndGlobalSection
33 | GlobalSection(SolutionProperties) = preSolution
34 | HideSolutionNode = FALSE
35 | EndGlobalSection
36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
37 | {F3C378FD-2A04-42FA-9B89-4F691B92CE0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {F3C378FD-2A04-42FA-9B89-4F691B92CE0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {F3C378FD-2A04-42FA-9B89-4F691B92CE0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {F3C378FD-2A04-42FA-9B89-4F691B92CE0D}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {3CA02AD8-6BD9-4E8A-857F-BCF800B12247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {3CA02AD8-6BD9-4E8A-857F-BCF800B12247}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {3CA02AD8-6BD9-4E8A-857F-BCF800B12247}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {3CA02AD8-6BD9-4E8A-857F-BCF800B12247}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {31A18443-E05B-47F1-812E-4FEECCCA06DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {31A18443-E05B-47F1-812E-4FEECCCA06DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {31A18443-E05B-47F1-812E-4FEECCCA06DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {31A18443-E05B-47F1-812E-4FEECCCA06DB}.Release|Any CPU.Build.0 = Release|Any CPU
49 | EndGlobalSection
50 | GlobalSection(NestedProjects) = preSolution
51 | {F3C378FD-2A04-42FA-9B89-4F691B92CE0D} = {0A6931CB-FA8E-4519-8EFA-61BF7E81E301}
52 | {3CA02AD8-6BD9-4E8A-857F-BCF800B12247} = {0A6931CB-FA8E-4519-8EFA-61BF7E81E301}
53 | {31A18443-E05B-47F1-812E-4FEECCCA06DB} = {0A6931CB-FA8E-4519-8EFA-61BF7E81E301}
54 | EndGlobalSection
55 | EndGlobal
56 |
--------------------------------------------------------------------------------
/src/Fabulous/Runner.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.Collections.Concurrent
5 |
6 | // Runners are responsible for the Model-Update part of MVU.
7 | // Runner is created for the component itself. No point in reusing a runner for another component
8 |
9 | /// Create a new Runner handling the update loop for the component
10 | type Runner<'arg, 'model, 'msg>(getState: unit -> 'model, setState: 'model -> unit, program: Program<'arg, 'model, 'msg>) =
11 | let mutable _activeSubs = Sub.Internal.empty
12 | let mutable _reentering = false
13 | let mutable _stopped = false
14 | let queue = ConcurrentQueue<'msg>()
15 |
16 | let onError (message, exn) =
17 | let ex = Exception(message, exn)
18 |
19 | if not(program.ExceptionHandler ex) then
20 | raise ex
21 |
22 | let processMsgs dispatch msg =
23 | let mutable lastMsg = ValueSome msg
24 |
25 | while not _stopped && lastMsg.IsSome do
26 | let model = getState()
27 | let newModel, cmd = program.Update(lastMsg.Value, model)
28 | let subs = program.Subscribe(newModel)
29 |
30 | setState newModel
31 |
32 | _activeSubs <- Sub.Internal.diff _activeSubs subs |> Sub.Internal.Fx.change onError dispatch
33 |
34 | Cmd.exec (fun ex -> onError("Error updating", ex)) dispatch cmd
35 |
36 | lastMsg <-
37 | match queue.TryDequeue() with
38 | | false, _ -> ValueNone
39 | | true, msg -> ValueSome msg
40 |
41 | let rec dispatch msg =
42 | try
43 | if _stopped then
44 | () // Message arrived after Runner got disposed, simply discard it
45 | else if _reentering then
46 | queue.Enqueue(msg)
47 | else
48 | _reentering <- true
49 | processMsgs dispatch msg
50 | _reentering <- false
51 | with ex ->
52 | _reentering <- false
53 |
54 | if not(program.ExceptionHandler ex) then
55 | reraise()
56 |
57 | let start arg =
58 | try
59 | _reentering <- true
60 |
61 | let model, cmd = program.Init(arg)
62 | setState model
63 |
64 | // Start the subscriptions
65 | let subs = program.Subscribe(model)
66 | _activeSubs <- Sub.Internal.diff _activeSubs subs |> Sub.Internal.Fx.change onError dispatch
67 |
68 | // Execute the commands
69 | Cmd.exec (fun ex -> onError("Error initializing", ex)) dispatch cmd
70 |
71 | _reentering <- false
72 | with ex ->
73 | _reentering <- false
74 |
75 | if not(program.ExceptionHandler(ex)) then
76 | reraise()
77 |
78 | let stop () =
79 | try
80 | _stopped <- true
81 | queue.Clear()
82 | Sub.Internal.Fx.stop onError _activeSubs
83 | _activeSubs <- Sub.Internal.empty
84 | with ex ->
85 | _reentering <- false
86 |
87 | if not(program.ExceptionHandler(ex)) then
88 | reraise()
89 |
90 | /// Start the Runner loop
91 | member _.Start(arg) = start arg
92 |
93 | /// Dispatch a message to the Runner loop
94 | member _.Dispatch(msg) = dispatch msg
95 |
96 | interface IDisposable with
97 | member _.Dispose() = stop()
98 |
--------------------------------------------------------------------------------
/src/Fabulous/Sub.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 |
5 | /// SubId - Subscription ID, alias for string list
6 | type SubId = string list
7 |
8 | /// Subscribe - Starts a subscription, returns IDisposable to stop it
9 | type Subscribe<'msg> = Dispatch<'msg> -> IDisposable
10 |
11 | /// Subscription - Generates new messages when running
12 | type Sub<'msg> = (SubId * Subscribe<'msg>) list
13 |
14 | module Sub =
15 |
16 | /// None - no subscriptions, also known as `[]`
17 | let none: Sub<'msg> = []
18 |
19 | /// Aggregate multiple subscriptions
20 | let batch (subs: Sub<'msg> list) : Sub<'msg> = List.concat subs
21 |
22 | /// When emitting the message, map to another type.
23 | /// To avoid ID conflicts with other components, scope SubIds with a prefix.
24 | let map (idPrefix: string) (f: 'a -> 'msg) (sub: Sub<'a>) : Sub<'msg> =
25 | sub
26 | |> List.map(fun (subId, subscribe) -> idPrefix :: subId, (fun dispatch -> subscribe(f >> dispatch)))
27 |
28 | module Internal =
29 |
30 | module SubId =
31 |
32 | let toString (subId: SubId) = String.Join("/", subId)
33 |
34 | module Fx =
35 |
36 | let warnDupe onError subId =
37 | let ex = exn "Duplicate SubId"
38 | onError("Duplicate SubId: " + SubId.toString subId, ex)
39 |
40 | let tryStop onError (subId, sub: IDisposable) =
41 | try
42 | sub.Dispose()
43 | with ex ->
44 | onError("Error stopping subscription: " + SubId.toString subId, ex)
45 |
46 | let tryStart onError dispatch (subId, start) : (SubId * IDisposable) option =
47 | try
48 | Some(subId, start dispatch)
49 | with ex ->
50 | onError("Error starting subscription: " + SubId.toString subId, ex)
51 | None
52 |
53 | let stop onError subs = subs |> List.iter(tryStop onError)
54 |
55 | let change onError dispatch (dupes, toStop, toKeep, toStart) =
56 | dupes |> List.iter(warnDupe onError)
57 | toStop |> List.iter(tryStop onError)
58 | let started = toStart |> List.choose(tryStart onError dispatch)
59 | List.append toKeep started
60 |
61 | module NewSubs =
62 |
63 | let (_dupes, _newKeys, _newSubs) as init = List.empty, Set.empty, List.empty
64 |
65 | let update (subId, start) (dupes, newKeys, newSubs) =
66 | if Set.contains subId newKeys then
67 | subId :: dupes, newKeys, newSubs
68 | else
69 | dupes, Set.add subId newKeys, (subId, start) :: newSubs
70 |
71 | let calculate subs = List.foldBack update subs init
72 |
73 | let empty = List.empty
74 |
75 | let diff (activeSubs: (SubId * IDisposable) list) (sub: Sub<'msg>) =
76 | let keys = activeSubs |> List.map fst |> Set.ofList
77 | let dupes, newKeys, newSubs = NewSubs.calculate sub
78 |
79 | if keys = newKeys then
80 | dupes, [], activeSubs, []
81 | else
82 | let toKeep, toStop =
83 | activeSubs |> List.partition(fun (k, _) -> Set.contains k newKeys)
84 |
85 | let toStart = newSubs |> List.filter(fun (k, _) -> not(Set.contains k keys))
86 | dupes, toStop, toKeep, toStart
87 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/ComponentContext.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.ComponentModel
5 |
6 | (*
7 | ARCHITECTURE NOTES:
8 |
9 | Conceptually, a ComponentContext is an array containing all the current values for each state inside the component.
10 | Each state is associated with a index key that it can use to retrieve or update its value.
11 |
12 | The ComponentContext is meant to be attached to a Component instance, and passed implicitly in the body of the component
13 | where it will be accessible through let! bindings.
14 |
15 | Given each state is assigned to a specific index and that Components will most likely have a fixed number of bindings,
16 | we can leverage the inlining capabilities of the ComponentBuilder to create an array with the right size.
17 | *)
18 |
19 | ///
20 | /// Holds the values for the various states of a component.
21 | ///
22 | []
23 | type ComponentContext(initialSize: int) =
24 | static let mutable nextId = 0
25 |
26 | static let getNextId () =
27 | nextId <- nextId + 1
28 | nextId
29 |
30 | let id = getNextId()
31 | let mutable values = Array.zeroCreate initialSize
32 | let disposables = System.Collections.Generic.Dictionary()
33 |
34 | let renderNeeded = Event()
35 |
36 | // We assume that most components will have few values, so initialize it with a small array
37 | new() = new ComponentContext(3)
38 |
39 | member this.Id = id
40 |
41 | member this.RenderNeeded = renderNeeded.Publish
42 | member this.NeedsRender() = renderNeeded.Trigger()
43 |
44 | member private this.ResizeIfNeeded(count: int) =
45 | // If the array is already big enough, we don't need to do anything
46 | // Otherwise, we create a new array and copy the values from the old one
47 | // It is assumed the component will have a stable amount of values, so this should not happen often
48 | if values.Length < count then
49 | let newLength = max (values.Length * 2) count
50 | let newArray = Array.zeroCreate newLength
51 | newArray[.. values.Length - 1] <- values
52 | values <- newArray
53 |
54 | member this.TryGetValue<'T>(key: int) =
55 | this.ResizeIfNeeded(key + 1)
56 |
57 | let value = values[key]
58 |
59 | if isNull value then
60 | ValueNone
61 | else
62 | ValueSome(unbox<'T> value)
63 |
64 | []
65 | member this.SetValueInternal(key: int, value: 'T) = values[key] <- box value
66 |
67 | member this.SetValue(key: int, value: 'T) =
68 | this.SetValueInternal(key, value)
69 | this.NeedsRender()
70 |
71 | member this.LinkDisposable<'T when 'T :> IDisposable>(key: string, disposable: unit -> 'T) =
72 | if disposables.ContainsKey(key) then
73 | disposables[key] :?> 'T
74 | else
75 | let disposable = disposable()
76 | disposables[key] <- disposable
77 | disposable
78 |
79 | member this.Dispose() =
80 | for disposable in disposables do
81 | disposable.Value.Dispose()
82 |
83 | disposables.Clear()
84 |
85 | for value in values do
86 | if value :? IDisposable then
87 | (value :?> IDisposable).Dispose()
88 |
89 | values <- Array.empty
90 |
91 | interface IDisposable with
92 | member this.Dispose() = this.Dispose()
93 |
94 | []
95 | type Context private () = class end
96 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/State.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System.ComponentModel
4 | open System.Runtime.CompilerServices
5 |
6 | type StateRequest<'T> = delegate of unit -> 'T
7 |
8 | /// DESIGN: StateValue<'T> is meant to be very short lived.
9 | /// It is created on Bind (let!) and destroyed at the end of a single ViewBuilder CE execution.
10 | /// Due to its nature, it is very likely it will be captured by a closure and allocated to the memory heap when it's not needed.
11 | ///
12 | /// e.g.
13 | ///
14 | /// Button("Increment", fun () -> stateValue.Set(stateValue.Current + 1))
15 | ///
16 | /// will become
17 | ///
18 | /// class Closure {
19 | /// public StateValue stateValue; // Storing a struct on a class will allocate it on the heap
20 | ///
21 | /// public void Invoke() {
22 | /// stateValue.Set(stateValue.Current + 1);
23 | /// }
24 | /// }
25 | ///
26 | /// class Program {
27 | /// public void View()
28 | /// {
29 | /// var stateValue = new StateValue(...);
30 | ///
31 | /// // This will allocate both the closure and the stateValue on the heap
32 | /// // which the GC will have to clean up later
33 | /// var closure = new Closure(stateValue = stateValue);
34 | ///
35 | /// return Button("Increment", closure);
36 | /// }
37 | /// }
38 | ///
39 | ///
40 | /// The Set method is therefore marked inlinable to avoid creating a closure capturing StateValue<'T>
41 | /// Instead the closure will only capture Context (already a reference type), Key (int) and Current (can be consider to be obj).
42 | /// The compiler will rewrite the lambda as follow:
43 | ///
44 | /// Button("Increment", fun () -> ctx.SetValue(key, current + 1))
45 | ///
46 | /// StateValue<'T> is no longer involved in the closure and will be kept on the stack.
47 | ///
48 | /// One constraint of inlining is to have all used fields public: Context, Key, Current
49 | /// But we don't wish to expose the Context and Key fields to the user, so we mark them as EditorBrowsable.Never
50 | []
51 | type StateValue<'T> =
52 | []
53 | val public Context: ComponentContext
54 |
55 | []
56 | val public Key: int
57 |
58 | val public Current: 'T
59 |
60 | new(ctx, key, value) =
61 | { Context = ctx
62 | Key = key
63 | Current = value }
64 |
65 | member inline this.Set(value: 'T) = this.Context.SetValue(this.Key, value)
66 |
67 | []
68 | module StateBuilders =
69 | type Context with
70 |
71 | static member inline State(defaultValue: 'T) =
72 | StateRequest<'T>(fun () -> defaultValue)
73 |
74 | []
75 | type StateExtensions =
76 | []
77 | static member inline Bind
78 | (
79 | _: ComponentBuilder<'parentMsg, 'marker>,
80 | [] fn: StateRequest<'T>,
81 | [] continuation: StateValue<'T> -> ComponentBodyBuilder<'msg, 'marker>
82 | ) =
83 | ComponentBodyBuilder<'msg, 'marker>(fun envContext treeContext context bindings ->
84 | let key = int bindings
85 |
86 | let value =
87 | match context.TryGetValue<'T>(key) with
88 | | ValueSome value -> value
89 | | ValueNone ->
90 | let value = fn.Invoke()
91 | context.SetValue(key, value)
92 | value
93 |
94 | let stateValue = StateValue<'T>(context, key, value)
95 |
96 | (continuation stateValue).Invoke(envContext, treeContext, context, bindings + 1))
97 |
--------------------------------------------------------------------------------
/src/Fabulous/Memo.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open Fabulous.ScalarAttributeDefinitions
5 |
6 | module Memo =
7 |
8 | type internal MemoData =
9 | {
10 | /// Captures data that memoization depends on
11 | KeyData: obj
12 |
13 | // comparer that remembers KeyType internally
14 | KeyComparer: obj -> obj -> bool
15 |
16 | /// wrapped untyped lambda that users provide
17 | CreateWidget: obj -> Widget
18 |
19 | /// Captures type of data that memoization depends on
20 | KeyType: Type
21 |
22 | /// Captures type of the marker memoized function produces
23 | MarkerType: Type
24 | }
25 |
26 | type Memoized<'t> = { phantom: 't }
27 |
28 | let inline private compareAttributes (prev: MemoData) (next: MemoData) : ScalarAttributeComparison =
29 | match (prev.KeyType = next.KeyType, prev.MarkerType = next.MarkerType) with
30 | | true, true ->
31 | match next.KeyComparer next.KeyData prev.KeyData with
32 | | true -> ScalarAttributeComparison.Identical
33 | | false -> ScalarAttributeComparison.Different
34 | | _ -> ScalarAttributeComparison.Different
35 |
36 | let inline private updateNode _ (data: MemoData voption) (node: IViewNode) : unit =
37 | match data with
38 | | ValueSome memoData ->
39 | let memoizedWidget = memoData.CreateWidget memoData.KeyData
40 |
41 | let prevWidget =
42 | match node.MemoizedWidget with
43 | | Some widget -> ValueSome(widget)
44 | | _ -> ValueNone
45 |
46 | node.MemoizedWidget <- Some memoizedWidget
47 |
48 | Reconciler.update node.TreeContext.CanReuseView prevWidget memoizedWidget node
49 |
50 | | ValueNone -> ()
51 |
52 | let private MemoAttributeKey =
53 | SimpleScalarAttributeDefinition.CreateAttributeData(compareAttributes, updateNode)
54 | |> AttributeDefinitionStore.registerScalar
55 |
56 | let inline private getMemoData (widget: Widget) : MemoData =
57 | match widget.ScalarAttributes with
58 | | [| attr |] -> attr.Value :?> MemoData
59 | | _ -> failwith "Memo widget cannot have extra attributes"
60 |
61 | let internal canReuseMemoizedWidget prev next =
62 | (getMemoData prev).MarkerType = (getMemoData next).MarkerType
63 |
64 | let internal MemoAttribute: SimpleScalarAttributeDefinition =
65 | { Key = MemoAttributeKey
66 | Name = "MemoAttribute" }
67 |
68 | let internal MemoWidgetKey = WidgetDefinitionStore.getNextKey()
69 |
70 | // Memo isn't allowed in lists, TargetType will never get called,
71 | // so Unchecked.defaultof is an acceptable value
72 | let private widgetDefinition: WidgetDefinition =
73 | { Key = MemoWidgetKey
74 | Name = "Memo"
75 | TargetType = Unchecked.defaultof<_>
76 | CreateView =
77 | fun (widget, treeContext, envContext, parentNode) ->
78 |
79 | let memoData = getMemoData widget
80 |
81 | let memoizedWidget = memoData.CreateWidget memoData.KeyData
82 |
83 | let memoizedDef = WidgetDefinitionStore.get memoizedWidget.Key
84 |
85 | let struct (node, view) =
86 | memoizedDef.CreateView(memoizedWidget, treeContext, envContext, parentNode)
87 |
88 | // store widget that was used to produce this node
89 | // to pass it to reconciler later on
90 | node.MemoizedWidget <- Some memoizedWidget
91 | struct (node, view)
92 | AttachView = fun (_widget, _treeContext, _envContext, _parentNode, _view) -> failwith "Memo widget cannot be attached" }
93 |
94 | WidgetDefinitionStore.set MemoWidgetKey widgetDefinition
95 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Widget.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 |
5 | module Component' =
6 | let Data =
7 | Attributes.defineSimpleScalar "Component_Data" ScalarAttributeComparers.noCompare (fun _ _ _ -> ())
8 |
9 | let WidgetKey =
10 | let key = WidgetDefinitionStore.getNextKey()
11 |
12 | let definition =
13 | { Key = key
14 | Name = "Component"
15 | TargetType = typeof
16 | CreateView =
17 | fun (widget, envContext, treeContext, _) ->
18 | match widget.ScalarAttributes with
19 | | [||] -> failwith "Component widget must have a body"
20 | | attrs ->
21 | let data =
22 | let scalarAttrsOpt =
23 | attrs |> Array.tryFind(fun scalarAttr -> scalarAttr.Key = Data.Key)
24 |
25 | match scalarAttrsOpt with
26 | | Some attr -> attr.Value :?> ComponentData
27 | | None -> failwith "Component widget must have a body"
28 |
29 | let envContext = new EnvironmentContext(treeContext.Logger, envContext)
30 | let context = new ComponentContext()
31 | let comp = new Component(Data.Key, envContext, treeContext, context, data.Body)
32 | let struct (node, view) = comp.CreateView(ValueSome widget)
33 |
34 | treeContext.SetComponent comp view
35 |
36 | struct (node, view)
37 | AttachView =
38 | fun (widget, envContext, treeContext, _, view) ->
39 | match widget.ScalarAttributes with
40 | | [||] -> failwith "Component widget must have a body"
41 | | attrs ->
42 | let data =
43 | let scalarAttrsOpt =
44 | attrs |> Array.tryFind(fun scalarAttr -> scalarAttr.Key = Data.Key)
45 |
46 | match scalarAttrsOpt with
47 | | Some attr -> attr.Value :?> ComponentData
48 | | None -> failwith "Component widget must have a body"
49 |
50 | let envContext = new EnvironmentContext(treeContext.Logger, envContext)
51 | let context = new ComponentContext()
52 | let comp = new Component(Data.Key, envContext, treeContext, context, data.Body)
53 | let node = comp.AttachView(widget, view)
54 |
55 | treeContext.SetComponent comp view
56 |
57 | node }
58 |
59 | WidgetDefinitionStore.set key definition
60 | key
61 |
62 | let canReuseComponent (prev: Widget) (curr: Widget) =
63 | let prevData =
64 | match prev.ScalarAttributes with
65 | | [||] -> failwith "Component widget must have a body"
66 | | attrs ->
67 | let scalarAttrsOpt =
68 | attrs |> Array.tryFind(fun scalarAttr -> scalarAttr.Key = Data.Key)
69 |
70 | match scalarAttrsOpt with
71 | | None -> failwithf "Component widget must have a body"
72 | | Some value -> value.Value :?> ComponentData
73 |
74 | let currData =
75 | match curr.ScalarAttributes with
76 | | [||] -> failwith "Component widget must have a body"
77 | | attrs ->
78 | let scalarAttrsOpt =
79 | attrs |> Array.tryFind(fun scalarAttr -> scalarAttr.Key = Data.Key)
80 |
81 | match scalarAttrsOpt with
82 | | None -> failwithf "Component widget must have a body"
83 | | Some value -> value.Value :?> ComponentData
84 |
85 | // NOTE: Somehow using = here crashes the app and prevents debugging...
86 | Object.Equals(prevData.Key, currData.Key)
87 |
--------------------------------------------------------------------------------
/src/Fabulous/EnvironmentContext.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.Collections.Generic
5 |
6 | []
7 | type EnvironmentValueChanged(originEnvId: Guid, fromUserCode: bool, key: EnvironmentAttributeKey, value: obj voption) =
8 | member this.OriginEnvId = originEnvId
9 | member this.FromUserCode = fromUserCode
10 | member this.Key = key
11 | member this.Value = value
12 |
13 | []
14 | type EnvironmentKey<'T>(key: string) =
15 | member this.Key = EnvironmentAttributeKey key
16 |
17 | and [] EnvironmentContext(logger: Logger, inheritedContext: EnvironmentContext) =
18 | let id = Guid.NewGuid()
19 | let values = Dictionary()
20 | let valueChanged = Event()
21 |
22 | do
23 | if inheritedContext = null then
24 | logger.Debug("EnvironmentContext '{0}' created", id)
25 | else
26 | logger.Debug("EnvironmentContext '{0}' created and inherited from '{1}'", id, inheritedContext.Id)
27 |
28 | let valuePropagationSubscription =
29 | if inheritedContext = null then
30 | null
31 | else
32 | inheritedContext.ValueChanged.Subscribe(fun args ->
33 | let (EnvironmentAttributeKey key) = args.Key
34 | logger.Debug("Env '{0}': Propagating '{1}' change from '{2}'", id, key, args.OriginEnvId)
35 | valueChanged.Trigger(args))
36 |
37 | new(logger: Logger) = new EnvironmentContext(logger, null)
38 |
39 | member this.Id = id
40 |
41 | member private this.TryGetValue<'T>(key: EnvironmentAttributeKey) =
42 | if values.ContainsKey(key) then
43 | ValueSome(unbox<'T> values[key])
44 | elif inheritedContext <> null then
45 | inheritedContext.TryGetValue(key)
46 | else
47 | ValueNone
48 |
49 | member internal this.SetInternal<'T>(key: EnvironmentAttributeKey, value: 'T, fromUserCode: bool) =
50 | let (EnvironmentAttributeKey envKey) = key
51 | logger.Debug("EnvironmentContext '{0}' set value '{1}' to '{2}'", id, envKey, value)
52 | let boxedValue = box value
53 | values[key] <- boxedValue
54 | valueChanged.Trigger(EnvironmentValueChanged(id, fromUserCode, key, ValueSome boxedValue))
55 |
56 | member internal this.RemoveInternal(key: EnvironmentAttributeKey, fromUserCode: bool) =
57 | if values.Remove(key) then
58 | valueChanged.Trigger(EnvironmentValueChanged(id, fromUserCode, key, ValueNone))
59 |
60 | member this.ValueChanged: IEvent = valueChanged.Publish
61 |
62 | member this.Get<'T>(key: EnvironmentKey<'T>) =
63 | match this.TryGetValue<'T>(key.Key) with
64 | | ValueSome value -> value
65 | | ValueNone ->
66 | let (EnvironmentAttributeKey key) = key.Key
67 | failwithf $"EnvironmentContext '{id}' does not contain value for key '{key}'"
68 |
69 | member this.Set<'T>(key: EnvironmentKey<'T>, value: 'T, ?fromUserCode: bool) =
70 | let fromUserCode = defaultArg fromUserCode true
71 |
72 | if values.ContainsKey(key.Key) || inheritedContext = null then
73 | let (EnvironmentAttributeKey envKey) = key.Key
74 | logger.Debug(envKey, "EnvironmentContext '{0}' set value '{1}' to '{2}'", id, key, value)
75 | let boxedValue = box value
76 | values[key.Key] <- boxedValue
77 | valueChanged.Trigger(EnvironmentValueChanged(id, fromUserCode, key.Key, ValueSome boxedValue))
78 | else
79 | inheritedContext.Set<'T>(key, value, fromUserCode)
80 |
81 | interface IDisposable with
82 | member this.Dispose() =
83 | logger.Debug("EnvironmentContext '{0}' disposed", id)
84 |
85 | if valuePropagationSubscription <> null then
86 | valuePropagationSubscription.Dispose()
87 |
88 | for value in values.Values do
89 | if value :? IDisposable then
90 | (value :?> IDisposable).Dispose()
91 |
92 | values.Clear()
93 |
--------------------------------------------------------------------------------
/src/Fabulous/Primitives.fs:
--------------------------------------------------------------------------------
1 | (* Dev notes:
2 |
3 | The types in this file will be the ones used the most internally by Fabulous.
4 |
5 | To enable the best performance possible, we want to avoid allocating them on
6 | the heap as must as possible (meaning they should be structs where possible)
7 | Also we want to avoid cache line misses, in that end, we make sure each struct
8 | can fit on a L1/L2 cache size by making those structs fit on 64 bits.
9 |
10 | Having those performance constraints prevents us for using inheritance
11 | or using interfaces on these structs *)
12 |
13 | namespace Fabulous
14 |
15 | /// Strongly types a scalar attribute key
16 | []
17 | type scalarAttributeKey
18 |
19 | /// Strongly types a widget attribute key
20 | []
21 | type widgetAttributeKey
22 |
23 | /// Strongly types a widget collection attribute key
24 | []
25 | type widgetCollectionAttributeKey
26 |
27 | /// Key identifying a scalar attribute (e.g. Text, Image, etc.)
28 | type ScalarAttributeKey = int
29 |
30 | /// Key identifying a widget attribute (e.g. Content, etc.)
31 | type WidgetAttributeKey = int
32 |
33 | /// Key identifying a widget collection attribute (e.g. Children, Items, etc.)
34 | type WidgetCollectionAttributeKey = int
35 |
36 | /// Key identifying an environment attribute (e.g. Theme, etc.)
37 | type EnvironmentAttributeKey = EnvironmentAttributeKey of string
38 |
39 | module ScalarAttributeKey =
40 | []
41 | type Kind =
42 | | Boxed // 1
43 | | Inline // 2
44 |
45 | module Code =
46 | []
47 | // 1 <<< 30
48 | let Boxed = 1073741824
49 |
50 | []
51 | // 2 <<< 30
52 | let Inline = -2147483648
53 |
54 | []
55 | // 3 <<< 30
56 | let CodeMask = -1073741824
57 |
58 | []
59 | // System.Int32.MaxValue >>> 2
60 | let KeyMask = 536870911
61 |
62 | let inline getKind (key: ScalarAttributeKey) : Kind =
63 | match (int key) &&& Code.Inline with
64 | | Code.Inline -> Inline
65 | | _ -> Boxed
66 |
67 | let inline getKeyValue (key: ScalarAttributeKey) : int = int key &&& Code.KeyMask
68 |
69 | let inline compare (a: ScalarAttributeKey) (b: ScalarAttributeKey) =
70 | let a = int a
71 | let b = int b
72 | a.CompareTo b
73 |
74 | module WidgetAttributeKey =
75 | let inline compare (a: WidgetAttributeKey) (b: WidgetAttributeKey) =
76 | let a = int a
77 | let b = int b
78 | a.CompareTo b
79 |
80 | module WidgetCollectionAttributeKey =
81 | let inline compare (a: WidgetCollectionAttributeKey) (b: WidgetCollectionAttributeKey) =
82 | let a = int a
83 | let b = int b
84 | a.CompareTo b
85 |
86 | module EnvironmentAttributeKey =
87 | let inline compare (EnvironmentAttributeKey a) (EnvironmentAttributeKey b) = a.CompareTo b
88 |
89 | type WidgetKey = int
90 | type StateKey = int
91 | type ViewAdapterKey = int
92 |
93 | /// Represents a value for a property of a widget
94 | []
95 | type ScalarAttribute =
96 | {
97 | Key: ScalarAttributeKey
98 | #if DEBUG
99 | DebugName: string
100 | #endif
101 | /// Stores the value as object (boxed), prefer NumericValue when possible
102 | Value: obj
103 | /// Stores the value in a numeric form for faster performance (no boxing)
104 | NumericValue: uint64
105 | }
106 |
107 | /// Represents a single child of a widget
108 | and [] WidgetAttribute =
109 | { Key: WidgetAttributeKey
110 | #if DEBUG
111 | DebugName: string
112 | #endif
113 | Value: Widget }
114 |
115 | /// Represents a collection of children of a widget
116 | and [] WidgetCollectionAttribute =
117 | { Key: WidgetCollectionAttributeKey
118 | #if DEBUG
119 | DebugName: string
120 | #endif
121 | Value: ArraySlice }
122 |
123 | /// Represents an environment value of a widget
124 | and [] EnvironmentAttribute =
125 | {
126 | Key: EnvironmentAttributeKey
127 | #if DEBUG
128 | DebugName: string
129 | #endif
130 | /// Stores the value as object (boxed)
131 | Value: obj
132 | }
133 |
134 | /// Represents a virtual UI element such as a Label, a Button, etc.
135 | and [] Widget =
136 | { Key: WidgetKey
137 | #if DEBUG
138 | DebugName: string
139 | #endif
140 | ScalarAttributes: ScalarAttribute[]
141 | WidgetAttributes: WidgetAttribute[]
142 | WidgetCollectionAttributes: WidgetCollectionAttribute[]
143 | EnvironmentAttributes: EnvironmentAttribute[] }
144 |
--------------------------------------------------------------------------------
/src/Fabulous.Tests/APISketchTests/TestUI.Platform.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous.Tests.APISketchTests
2 |
3 | module Platform =
4 |
5 | open System.Collections.Generic
6 |
7 |
8 | type TestViewElement() =
9 | member val AutomationId: string = "" with get, set
10 | member val PropertyBag = Dictionary()
11 |
12 | type IText =
13 | abstract member Text: string with get, set
14 | abstract member TextColor: string with get, set
15 |
16 | type ContainerHandler = unit -> unit
17 |
18 | type IContainer =
19 | abstract member Children: List
20 | abstract AddTapListener: ContainerHandler -> int
21 | abstract RemoveTapListener: int -> unit
22 |
23 | type ButtonHandler = unit -> unit
24 |
25 | type IButton =
26 | abstract AddPressListener: ButtonHandler -> int
27 | abstract RemovePressListener: int -> unit
28 | abstract AddTapListener: ButtonHandler -> int
29 | abstract RemoveTapListener: int -> unit
30 | abstract AddTap2Listener: ButtonHandler -> int
31 | abstract RemoveTap2Listener: int -> unit
32 |
33 | type LabelChangeList =
34 | | TextSet of string
35 | | ColorSet of string
36 |
37 |
38 | type TestLabel() =
39 | inherit TestViewElement()
40 |
41 | let mutable text = ""
42 | let mutable textColor = ""
43 | member val record = false with get, set
44 |
45 | member val changeList = [] with get, set
46 |
47 | interface IText with
48 | member x.Text
49 | with get () = text
50 | and set value =
51 | if x.record then
52 | x.changeList <- List.append x.changeList [ TextSet value ]
53 |
54 | text <- value
55 |
56 | member x.TextColor
57 | with get () = textColor
58 | and set value =
59 | if x.record then
60 | x.changeList <- List.append x.changeList [ ColorSet value ]
61 |
62 | textColor <- value
63 |
64 | type TestStack() =
65 | inherit TestViewElement()
66 | let mutable tapCounter: int = 1
67 | let tapHandlers = Dictionary()
68 |
69 | member _.Tap() =
70 | for handler in Array.ofSeq(tapHandlers.Values) do
71 | handler()
72 |
73 | interface IContainer with
74 | member val Children = List()
75 |
76 | member this.AddTapListener(handler) =
77 | tapHandlers.Add(tapCounter, handler)
78 | tapCounter <- tapCounter + 1
79 | tapCounter - 1
80 |
81 | member this.RemoveTapListener(id) = tapHandlers.Remove(id) |> ignore
82 |
83 | type TestButton() =
84 | inherit TestViewElement()
85 | let mutable pressCounter: int = 1
86 | let mutable tapCounter: int = 1
87 | let mutable tap2Counter: int = 1
88 | let pressHandlers = Dictionary()
89 | let tapHandlers = Dictionary()
90 | let tap2Handlers = Dictionary()
91 |
92 | member _.Press() =
93 | for handler in Array.ofSeq(pressHandlers.Values) do
94 | handler()
95 |
96 | member _.Tap() =
97 | for handler in Array.ofSeq(tapHandlers.Values) do
98 | handler()
99 |
100 | member _.Tap2() =
101 | for handler in Array.ofSeq(tap2Handlers.Values) do
102 | handler()
103 |
104 | interface IText with
105 | member val Text = "" with get, set
106 | member val TextColor = "" with get, set
107 |
108 | interface IButton with
109 | member this.AddPressListener(handler) =
110 | pressHandlers.Add(pressCounter, handler)
111 | pressCounter <- pressCounter + 1
112 | pressCounter - 1
113 |
114 | member this.RemovePressListener(id) = pressHandlers.Remove(id) |> ignore
115 |
116 | member this.AddTapListener(handler) =
117 | tapHandlers.Add(tapCounter, handler)
118 | tapCounter <- tapCounter + 1
119 | tapCounter - 1
120 |
121 | member this.RemoveTapListener(id) = tapHandlers.Remove(id) |> ignore
122 |
123 | member this.AddTap2Listener(handler) =
124 | tap2Handlers.Add(tap2Counter, handler)
125 | tap2Counter <- tap2Counter + 1
126 | tap2Counter - 1
127 |
128 | member this.RemoveTap2Listener(id) = tap2Handlers.Remove(id) |> ignore
129 |
130 |
131 | type TestNumericBag() =
132 | inherit TestViewElement()
133 | member val valueOne = 0UL with get, set
134 | member val valueTwo = 0UL with get, set
135 | member val valueThree = 0.0 with get, set
136 |
--------------------------------------------------------------------------------
/src/Fabulous/Program.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.Diagnostics
5 |
6 | /// Configuration of the Fabulous application
7 | type Program<'arg, 'model, 'msg> =
8 | {
9 | /// Give the initial state for the application
10 | Init: 'arg -> 'model * Cmd<'msg>
11 | /// Update the application state based on a message
12 | Update: 'msg * 'model -> 'model * Cmd<'msg>
13 | /// Add a subscription that can dispatch messages
14 | Subscribe: 'model -> Sub<'msg>
15 | /// Configuration for logging all output messages from Fabulous
16 | Logger: Logger
17 | /// Exception handler for all uncaught exceptions happening in the MVU loop.
18 | /// Returns true if the exception was handled, false otherwise.
19 | ExceptionHandler: exn -> bool
20 | }
21 |
22 | type Program<'arg, 'model, 'msg, 'marker when 'msg: equality> =
23 | {
24 | State: Program<'arg, 'model, 'msg>
25 | /// Render the application state
26 | View: 'model -> WidgetBuilder<'msg, 'marker>
27 | /// Indicates if a previous Widget's view can be reused
28 | CanReuseView: Widget -> Widget -> bool
29 | /// Runs the View function on the main thread
30 | SyncAction: (unit -> unit) -> unit
31 | }
32 |
33 | module ProgramDefaults =
34 | let defaultLogger () =
35 | let log (level, message) =
36 | let traceLevel =
37 | match level with
38 | | LogLevel.Debug -> "Debug"
39 | | LogLevel.Info -> "Information"
40 | | LogLevel.Warn -> "Warning"
41 | | LogLevel.Error -> "Error"
42 | | _ -> "Error"
43 |
44 | Trace.WriteLine(message, traceLevel)
45 |
46 | { Log = log
47 | MinLogLevel = LogLevel.Error }
48 |
49 | let defaultExceptionHandler exn =
50 | Trace.WriteLine(String.Format("Unhandled exception: {0}", exn.ToString()), "Debug")
51 | false
52 |
53 | module Program =
54 | let inline private define (init: 'arg -> 'model * Cmd<'msg>) (update: 'msg -> 'model -> 'model * Cmd<'msg>) =
55 | { Init = init
56 | Update = (fun (msg, model) -> update msg model)
57 | Subscribe = fun _ -> Sub.none
58 | Logger = ProgramDefaults.defaultLogger()
59 | ExceptionHandler = ProgramDefaults.defaultExceptionHandler }
60 |
61 | /// Create a program using an MVU loop
62 | let stateful (init: 'arg -> 'model) (update: 'msg -> 'model -> 'model) =
63 | define (fun arg -> init arg, Cmd.none) (fun msg model -> update msg model, Cmd.none)
64 |
65 | /// Create a program using an MVU loop
66 | let statefulWithCmd (init: 'arg -> 'model * Cmd<'msg>) (update: 'msg -> 'model -> 'model * Cmd<'msg>) = define init update
67 |
68 | /// Create a program using an MVU loop. Add support for CmdMsg
69 | let statefulWithCmdMsg (init: 'arg -> 'model * 'cmdMsg list) (update: 'msg -> 'model -> 'model * 'cmdMsg list) (mapCmd: 'cmdMsg -> Cmd<'msg>) =
70 | let mapCmds cmdMsgs = cmdMsgs |> List.map mapCmd |> Cmd.batch
71 | define (fun arg -> let m, c = init arg in m, mapCmds c) (fun msg model -> let m, c = update msg model in m, mapCmds c)
72 |
73 | /// Subscribe to external source of events, overrides existing subscription.
74 | /// Return the subscriptions that should be active based on the current model.
75 | /// Subscriptions will be started or stopped automatically to match.
76 | let withSubscription (subscribe: 'model -> Sub<'msg>) (program: Program<'arg, 'model, 'msg>) = { program with Subscribe = subscribe }
77 |
78 | /// Map existing subscription to external source of events.
79 | let mapSubscription map (program: Program<'arg, 'model, 'msg>) =
80 | { program with
81 | Subscribe = map program.Subscribe }
82 |
83 | /// Configure how the output messages from Fabulous will be handled
84 | let withLogger (logger: Logger) (program: Program<'arg, 'model, 'msg>) = { program with Logger = logger }
85 |
86 | /// Trace all the updates to the debug output
87 | let withTrace (trace: string * string -> unit) (program: Program<'arg, 'model, 'msg>) =
88 | let traceInit arg =
89 | try
90 | let initModel, cmd = program.Init(arg)
91 | trace("Initial model: {0}", $"%0A{initModel}")
92 | initModel, cmd
93 | with e ->
94 | trace("Error in init function: {0}", $"%0A{e}")
95 | reraise()
96 |
97 | let traceUpdate (msg, model) =
98 | trace("Message: {0}", $"%0A{msg}")
99 |
100 | try
101 | let newModel, cmd = program.Update(msg, model)
102 | trace("Updated model: {0}", $"%0A{newModel}")
103 | newModel, cmd
104 | with e ->
105 | trace("Error in model function: {0}", $"%0A{e}")
106 | reraise()
107 |
108 | { program with
109 | Init = traceInit
110 | Update = traceUpdate }
111 |
112 | /// Configure how the unhandled exceptions happening during the execution of a Fabulous app with be handled
113 | let withExceptionHandler (handler: exn -> bool) (program: Program<'arg, 'model, 'msg>) =
114 | { program with
115 | ExceptionHandler = handler }
116 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | maintainers@fabulous.dev.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | **Note:** If these contribution guidelines are not followed your issue or PR might be closed, so
4 | please read these instructions carefully.
5 |
6 |
7 | ## Contribution types
8 |
9 |
10 | ### Bug Reports
11 |
12 | - If you find a bug, please first report it using [Github issues].
13 | - First check if there is not already an issue for it; duplicated issues will be closed.
14 |
15 |
16 | ### Bug Fix
17 |
18 | - If you'd like to submit a fix for a bug, please read the [How To](#how-to-contribute) for how to
19 | send a Pull Request.
20 | - Indicate on the open issue that you are working on fixing the bug and the issue will be assigned
21 | to you.
22 | - Write `Fixes #xxxx` in your PR text, where xxxx is the issue number (if there is one).
23 | - Include a test that isolates the bug and verifies that it was fixed.
24 |
25 |
26 | ### New Features
27 |
28 | - If you'd like to add a feature to the library that doesn't already exist, feel free to describe
29 | the feature in a new [GitHub issue].
30 | - You can also join us on [Discord] to discuss some initials thoughts.
31 | - If you'd like to implement the new feature, please wait for feedback from the project maintainers
32 | before spending too much time writing the code. In some cases, enhancements may not align well
33 | with the project future development direction.
34 | - Implement the code for the new feature and please read the [How To](#how-to-contribute).
35 |
36 |
37 | ### Documentation & Miscellaneous
38 |
39 | - If you have suggestions for improvements to the documentation, tutorial or examples (or something
40 | else), we would love to hear about it.
41 | - As always first file a [Github issue].
42 | - Implement the changes to the documentation, please read the [How To](#how-to-contribute).
43 |
44 |
45 | ## How To Contribute
46 |
47 |
48 | ### Requirements
49 |
50 | For a contribution to be accepted:
51 |
52 | - Format the code using
53 | ```
54 | dotnet tool restore
55 | dotnet fantomas -r src
56 | ```
57 | - Check that all tests pass: `dotnet test`;
58 | - Documentation should always be updated or added (if applicable);
59 | - Examples should always be updated or added (if applicable);
60 | - Tests should always be updated or added (if applicable).
61 |
62 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the issue
63 | or PR. You can still continue to add more commits to the branch you have sent the Pull Request from
64 | and it will be automatically reflected in the PR.
65 |
66 |
67 | ## Open an issue and fork the repository
68 |
69 | - If it is a bigger change or a new feature, first of all
70 | [file a bug or feature report][GitHub issue], so that we can discuss what direction to follow.
71 | - [Fork the project][fork guide] on GitHub.
72 | - Clone the forked repository to your local development machine
73 | (e.g. `git clone git@github.com:/Fabulous.git`).
74 |
75 |
76 | ### Environment Setup
77 |
78 | Setting up your environment for Fabulous is pretty easy.
79 | You will only need to install the [.NET 7.0 SDK] matching your CPU architecture (x64 or Arm64 for Mac M1).
80 |
81 | After .NET is installed, you can make sure Fabulous builds by executing the command line `dotnet build` at the Fabulous root folder.
82 |
83 | You can also pick any IDE you prefer to work on the codebase: Visual Studio (both Windows and macOS), Jetbrains Rider, or Visual Studio Code (with the [Ionide plugin]).
84 |
85 | ### Performing changes
86 |
87 | - Create a new local branch from `main` (e.g. `git checkout -b my-new-feature`)
88 | - Make your changes (try to split them up with one PR per feature/fix).
89 | - When committing your changes, make sure that each commit message is clear.
90 | - Push your new branch to your own fork into the same remote branch
91 | (e.g. `git push origin my-username.my-new-feature`, replace `origin` if you use another remote.)
92 |
93 |
94 | ### Open a pull request
95 |
96 | Go to the [pull request page of Fabulous][PRs] and in the top
97 | of the page it will ask you if you want to open a pull request from your newly created branch.
98 |
99 | The title of the pull request should be descriptive of the work you did.
100 |
101 |
102 | ## Maintainers
103 |
104 | These instructions are for the maintainers of Fabulous.
105 |
106 |
107 | ### Merging a pull request
108 |
109 | When merging a pull request, make sure that the title of the merge commit has a descriptive title.
110 |
111 |
112 | ### Creating a release
113 |
114 | There are a few things to think about when doing a release:
115 |
116 | - Search through the codebase for `[]` methods/fields and remove the ones that are marked
117 | for removal in the version that you are intending to release.
118 | - Create a PR containing the changes for removing the deprecated entities.
119 | - Update [CHANGELOG.md] with the latest fixes and features since the last release.
120 | - Go through the PRs with breaking changes and add migration documentation to the changelog.
121 | There should be migration docs on each PR, if they haven't been copied to the commit message.
122 | - Bump the version number in the [build workflow] file so Github can release nightly packages with the new version.
123 | - Make sure the build pipeline is succeeding on the main branch.
124 | - Once you are satisfied, create a new release via the [Github releases page], create a new tag with the version you want (eg. `2.2.0`), use the version number as the release name, paste the [CHANGELOG.md] section about this release into the description, and hit Submit.
125 | - The release pipeline will need to be approved by one of the maintainers with release rights.
126 |
127 |
128 | [GitHub issue]: https://github.com/fabulous-dev/fabulous/issues
129 | [GitHub issues]: https://github.com/fabulous-dev/fabulous/issues
130 | [GitHub releases page]: https://github.com/fabulous-dev/Fabulous/releases/new
131 | [PRs]: https://github.com/fabulous-dev/fabulous/pulls
132 | [fork guide]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
133 | [Discord]: https://discord.gg/bpTJMbSSYK
134 | [.NET 7.0 SDK]: https://dotnet.microsoft.com/en-us/download
135 | [Ionide plugin]: https://ionide.io/Editors/Code/overview.html
136 | [build workflow]: .github/workflows/build.yml
137 | [CHANGELOG.md]: CHANGELOG.md
--------------------------------------------------------------------------------
/src/Fabulous.Benchmarks/Benchmarks.fs:
--------------------------------------------------------------------------------
1 | module Fabulous.Tests.Benchmarks
2 |
3 | open BenchmarkDotNet.Attributes
4 |
5 | open BenchmarkDotNet.Running
6 | open Fabulous.Tests.APISketchTests.TestUI_Widgets
7 |
8 | open type View
9 |
10 |
11 | module NestedTreeCreation =
12 | []
13 | type Model = { depth: int }
14 |
15 | []
16 | type Msg = Depth of int
17 |
18 | let rec viewInner (depth: int) =
19 | // printfn $"view on {depth}"
20 |
21 | Stack() {
22 | if (depth > 0) then
23 | viewInner(depth - 1)
24 |
25 | Label($"label1:{depth}").textColor("red").automationId($"label1:{depth}")
26 |
27 | Label($"label2:{depth}").textColor("green").automationId($"label2:{depth}")
28 |
29 | Button($"btn: {depth}", Depth depth)
30 |
31 | if (depth > 0) then
32 | viewInner(depth - 2)
33 | }
34 |
35 | let view d = viewInner d
36 |
37 | // []
38 | []
39 | []
40 | // []
41 | // []
42 | type Benchmarks() =
43 | []
44 | member val depth = 0 with get, set
45 |
46 | []
47 | member x.CreateWidgets() = view x.depth
48 |
49 |
50 | module DiffingAttributes =
51 | // []
52 | type Model = { depth: int; counter: int }
53 |
54 | // []
55 | type Msg = IncBy of int
56 |
57 | let update msg model =
58 | match msg with
59 | | IncBy amount ->
60 | { model with
61 | counter = model.counter + amount }
62 |
63 | let rec viewInner depth counter =
64 | Stack() {
65 | Label($"label1:{counter} {depth}").textColor("red").automationId($"label1:{depth}")
66 |
67 | Label($"label2:{counter} {depth}").textColor("green").automationId($"label2:{depth}")
68 |
69 | Button($"btn: {depth}", IncBy 2)
70 |
71 | if (depth > 0) then
72 | viewInner (depth - 1) counter
73 |
74 | if (depth > 0) then
75 | viewInner (depth - 2) counter
76 | }
77 |
78 | let view model = viewInner model.depth model.counter
79 |
80 | []
81 | []
82 | type Benchmarks() =
83 | []
84 | member val depth = 0 with get, set
85 |
86 | []
87 | member x.ProcessMessages() =
88 | let program =
89 | StatefulWidget.mkSimpleView (fun () -> { depth = x.depth; counter = 0 }) update view
90 |
91 | let instance = Run.Instance program
92 |
93 | let _tree = instance.Start()
94 |
95 | for i in 1..100 do
96 | instance.ProcessMessage(IncBy i)
97 |
98 |
99 | module DiffingSmallScalars =
100 | []
101 | type Model = { depth: int; counter: uint64 }
102 |
103 | []
104 | type Msg = IncBy of uint64
105 |
106 | let update msg model =
107 | match msg with
108 | | IncBy amount ->
109 | { model with
110 | counter = model.counter + amount }
111 |
112 | let rec viewBoxedInner depth counter =
113 | // this is to emulate changing value only once per 5 updates
114 | let value = counter / 2UL
115 |
116 | Stack() {
117 | BoxedNumericBag(value, value, float value)
118 | BoxedNumericBag(value, value, float value)
119 | BoxedNumericBag(value, value, float value)
120 |
121 | if (depth > 0) then
122 | viewBoxedInner (depth - 1) counter
123 |
124 | if (depth > 0) then
125 | viewBoxedInner (depth - 2) counter
126 | }
127 |
128 | let rec viewInlineInner depth counter =
129 | // this is to emulate changing value only once per 5 updates
130 | let value = counter / 2UL
131 |
132 | Stack() {
133 | InlineNumericBag(value, value, float value)
134 | InlineNumericBag(value, value, float value)
135 | InlineNumericBag(value, value, float value)
136 |
137 | if (depth > 0) then
138 | viewInlineInner (depth - 1) counter
139 |
140 | if (depth > 0) then
141 | viewInlineInner (depth - 2) counter
142 | }
143 |
144 | let viewBoxed model =
145 | viewBoxedInner model.depth model.counter
146 |
147 | let viewInline model =
148 | viewInlineInner model.depth model.counter
149 |
150 | []
151 | []
152 | type Benchmarks() =
153 | []
154 | member val depth = 0 with get, set
155 |
156 | []
157 | member val boxed = true with get, set
158 |
159 | []
160 | member x.ProcessIncrements() =
161 | let program =
162 |
163 | let view = if x.boxed then viewBoxed else viewInline
164 |
165 | StatefulWidget.mkSimpleView (fun () -> { depth = x.depth; counter = 0UL }) update view
166 |
167 | let instance = Run.Instance program
168 |
169 | let _tree = instance.Start()
170 |
171 | for i in 1..100 do
172 | instance.ProcessMessage(IncBy 1UL)
173 |
174 |
175 |
176 | []
177 | let main _argv =
178 | // BenchmarkRunner.Run()
179 | // |> ignore
180 | //
181 | // BenchmarkRunner.Run()
182 | // |> ignore
183 |
184 | printfn "Hello"
185 |
186 | BenchmarkRunner.Run(typeof.Assembly) |> ignore
187 |
188 | 0 // return an integer exit code
189 |
190 | //[]
191 | //let mainProfile argv =
192 | //
193 | // let b = NestedTreeCreation.Benchmarks()
194 | // b.depth <- 25
195 | //
196 | // for _ in 0 .. 10 do
197 | // let widgets = b.CreateWidgets()
198 | // ()
199 | //
200 | // 0 // return an integer exit code
201 |
202 | //[]
203 | //let mainProfile argv =
204 | //
205 | // let b = DiffingAttributes.Benchmarks()
206 | // b.depth <- 10
207 | //
208 | // for _ in 0 .. 2 do
209 | // let widgets = b.ProcessMessages()
210 | // ()
211 | //
212 | // 0 // return an integer exit code
213 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | [](https://github.com/fabulous-dev/Fabulous/actions/workflows/build.yml) [](https://www.nuget.org/packages/Fabulous) [](https://www.nuget.org/packages/Fabulous) [](https://discord.gg/bpTJMbSSYK) [](https://twitter.com/FabulousAppDev)
11 |
12 | Fabulous is a modern declarative UI framework for crafting cross-platform mobile and desktop apps in .NET.
13 | It aims to bring you a great development experience and confidence in your code by combining an expressive UI syntax, the simple & robust Model-View-Update (MVU) architecture, and functional programming.
14 |
15 | ## Documentation
16 |
17 | The full documentation for Fabulous can be found at [docs.fabulous.dev](https://docs.fabulous.dev).
18 |
19 | Other useful links:
20 | - [The official Fabulous website](https://fabulous.dev)
21 | - [Get started](https://docs.fabulous.dev/get-started)
22 | - [API Reference](https://api.fabulous.dev)
23 | - [Contributor Guide](CONTRIBUTING.md)
24 |
25 | Additionally, we have the [Fabulous Discord server](https://discord.gg/bpTJMbSSYK) where you can ask any of your Fabulous related questions.
26 |
27 | ## About Fabulous
28 |
29 | We believe declarative UI, functional programming, and the MVU state management are a perfect fit for app development.
30 |
31 | Fabulous will help you create mobile and desktop apps quickly and with confidence thanks to declarative UI and the [MVU](https://zaid-ajaj.github.io/the-elmish-book/#/chapters/elm/) architecture, all in one single language: [F#](https://fsharp.org) - a functional programing language.
32 |
33 | Fabulous also aims to be performant by having low memory consumption and efficient view diffing mechanisms.
34 |
35 | Note that Fabulous itself does not provide any UI rendering. You'll need to combine it with another framework like:
36 | - [.NET MAUI](https://dotnet.microsoft.com/en-us/apps/maui) with [Fabulous.MauiControls](https://github.com/fabulous-dev/Fabulous.MauiControls)
37 | - [AvaloniaUI](https://avaloniaui.net) with [Fabulous.Avalonia](https://github.com/fabulous-dev/Fabulous.Avalonia)
38 |
39 | ### Declarative UI
40 |
41 | Typical UI development can be a nightmare if not done properly.
42 | It is generally created in one place, then mutated here and there based on the need and what the user is doing. Related UI pieces end up in several places, making it hard to mentally think of all the possibilities; until the inevitable race condition or bug due to an unintended user flow.
43 |
44 | Fabulous makes it easier to reason about UI thanks to its declarative UI inspired by SwiftUI.
45 | The UI of a component is defined in a single place and Fabulous will call it everytime the state of that component is changed.
46 |
47 | You don't need to think about how to mutate the UI, Fabulous will handle it for you to always match the latest UI you need.
48 |
49 | ```fs
50 | /// A simple Counter app made with Fabulous.MauiControls
51 | type Model =
52 | { Count: int }
53 |
54 | type Msg =
55 | | Increment
56 | | Decrement
57 |
58 | let view model =
59 | Application(
60 | ContentPage(
61 | "Counter app",
62 | VStack(spacing = 16.) {
63 | Image(Aspect.AspectFit, "fabulous.png")
64 |
65 | Label($"Count is {model.Count}")
66 |
67 | Button("Increment", Increment)
68 | Button("Decrement", Decrement)
69 | }
70 | )
71 | )
72 | ```
73 |
74 | ### MVU architecture
75 |
76 | MVU makes every state and transition between those states explicit.
77 | You don't need to worry about unintended actions that could lead to an invalid state which would crash the app.
78 |
79 | Instead, you can very easily model the state of your app or component and transitions between them using F# records and discriminated unions types.
80 | When starting, Fabulous will initialize the state. Then, when messages are being dispatched, Fabulous will let you transition from one state to the other given a specific message.
81 |
82 | If several messages are received at the same time, Fabulous will queue them to let you update the state properly.
83 |
84 | ```fs
85 | let init () =
86 | { Count = 0 }
87 |
88 | let update msg model =
89 | match msg with
90 | | Increment -> { model with Count = model.Count + 1 }
91 | | Decrement -> { model with Count = model.Count - 1 }
92 | ```
93 |
94 | And finally, given the functional nature of MVU, it is extremely simple to unit test each and every possible state of your application.
95 |
96 | ```fs
97 | []
98 | let ``When clicking the Increment button, increment the count by one``() =
99 | let previousState = { Count = 10 }
100 | let expectedState = { Count = 11 }
101 |
102 | let actualState = App.update Increment previousState
103 |
104 | actualState |> should equal expectedState
105 | ```
106 |
107 | ### Powered by .NET
108 |
109 | .NET is a very mature and broad framework by Microsoft. It can run on any device and platform, is very efficient, and has a vast ecosystem of open-source and licensed libraries, plugins, and other frameworks.
110 | You will be able to benefit from the .NET ecosystem by using 3rd party packages directly in your Fabulous application.
111 |
112 | ## Supporting this project
113 |
114 | The simplest way to show us your support is by giving the project a star.
115 |
116 | You can also support us by becoming our sponsor on the GitHub Sponsors program.
117 | This is a fantastic way to support all the efforts going into making Fabulous the best declarative UI framework for dotnet.
118 |
119 | If you need support see Commercial Support section below.
120 |
121 | ## Contributing
122 |
123 | Have you found a bug or have a suggestion of how to enhance Fabulous? Open an issue and we will take a look at it as soon as possible.
124 |
125 | Do you want to contribute with a PR? PRs are always welcome, just make sure to create it from the correct branch (main) and follow the [Contributor Guide](CONTRIBUTING.md).
126 |
127 | For bigger changes, or if in doubt, make sure to talk about your contribution to the team. Either via an issue, GitHub discussion, or reach out to the team either using the [Discord server](https://discord.gg/bpTJMbSSYK).
128 |
129 | ## Commercial support
130 |
131 | If you would like us to provide you with:
132 |
133 | - training and workshops,
134 | - support services,
135 | - and consulting services.
136 |
137 | Feel free to contact us: [support@fabulous.dev](mailto:support@fabulous.dev)
138 |
139 | ## Star History
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/src/Fabulous.Tests/APISketchTests/TestUI.Attributes.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous.Tests.APISketchTests
2 |
3 | module TestUI_Attributes =
4 |
5 | open System
6 | open Fabulous
7 | open Fabulous.ScalarAttributeDefinitions
8 | open Platform
9 |
10 | module Attributes =
11 |
12 | let definePressable name : ScalarAttributeDefinition =
13 | let key =
14 | ScalarAttributeDefinition.CreateAttributeData(
15 | (fun x -> x),
16 | ScalarAttributeComparers.noCompare,
17 | (fun _ newValueOpt node ->
18 |
19 | let btn = node.Target :?> IButton
20 |
21 | match node.TryGetHandler(name) with
22 | | ValueNone -> ()
23 | | ValueSome handler -> handler.Dispose()
24 |
25 | match newValueOpt with
26 | | ValueNone -> node.RemoveHandler(name)
27 |
28 | | ValueSome msg ->
29 | let handler () = Dispatcher.dispatch node msg
30 | let handlerId = btn.AddPressListener handler
31 |
32 | let disposable =
33 | { new IDisposable with
34 | member _.Dispose() = btn.RemovePressListener handlerId }
35 |
36 | node.SetHandler(name, disposable))
37 | )
38 | |> AttributeDefinitionStore.registerScalar
39 |
40 | { Key = key; Name = name }
41 |
42 | let defineTappable name : ScalarAttributeDefinition =
43 | let key =
44 | ScalarAttributeDefinition.CreateAttributeData(
45 | (fun x -> x),
46 | ScalarAttributeComparers.noCompare,
47 | (fun _ newValueOpt node ->
48 |
49 | let btn = node.Target :?> IButton
50 |
51 | match node.TryGetHandler(name) with
52 | | ValueNone -> ()
53 | | ValueSome handler -> handler.Dispose()
54 |
55 | match newValueOpt with
56 | | ValueNone -> node.RemoveHandler(name)
57 |
58 | | ValueSome msg ->
59 | let handler () = Dispatcher.dispatch node msg
60 | let handlerId = btn.AddTapListener handler
61 |
62 | let disposable =
63 | { new IDisposable with
64 | member _.Dispose() = btn.RemoveTapListener handlerId }
65 |
66 | node.SetHandler(name, disposable))
67 | )
68 | |> AttributeDefinitionStore.registerScalar
69 |
70 | { Key = key; Name = name }
71 |
72 | let defineTappable2 name : ScalarAttributeDefinition =
73 | let key =
74 | ScalarAttributeDefinition.CreateAttributeData(
75 | (fun x -> x),
76 | ScalarAttributeComparers.noCompare,
77 | (fun _ newValueOpt node ->
78 |
79 | let btn = node.Target :?> IButton
80 |
81 | match node.TryGetHandler(name) with
82 | | ValueNone -> ()
83 | | ValueSome handler -> handler.Dispose()
84 |
85 | match newValueOpt with
86 | | ValueNone -> node.RemoveHandler(name)
87 |
88 | | ValueSome msg ->
89 | let handler () = Dispatcher.dispatch node msg
90 | let handlerId = btn.AddTap2Listener handler
91 |
92 | let disposable =
93 | { new IDisposable with
94 | member _.Dispose() = btn.RemoveTap2Listener handlerId }
95 |
96 | node.SetHandler(name, disposable))
97 | )
98 | |> AttributeDefinitionStore.registerScalar
99 |
100 | { Key = key; Name = name }
101 |
102 | let defineContainerTappable name : ScalarAttributeDefinition =
103 | let key =
104 | ScalarAttributeDefinition.CreateAttributeData(
105 | (fun x -> x),
106 | ScalarAttributeComparers.noCompare,
107 | (fun _ newValueOpt node ->
108 |
109 | let btn = node.Target :?> IContainer
110 |
111 | match node.TryGetHandler(name) with
112 | | ValueNone -> ()
113 | | ValueSome handler -> handler.Dispose()
114 |
115 | match newValueOpt with
116 | | ValueNone -> node.RemoveHandler(name)
117 |
118 | | ValueSome msg ->
119 | let handler () = Dispatcher.dispatch node msg
120 | let handlerId = btn.AddTapListener handler
121 |
122 | let disposable =
123 | { new IDisposable with
124 | member _.Dispose() = btn.RemoveTapListener handlerId }
125 |
126 | node.SetHandler(name, disposable))
127 | )
128 | |> AttributeDefinitionStore.registerScalar
129 |
130 | { Key = key; Name = name }
131 |
132 |
133 |
134 |
135 | // --------------- Actual Properties ---------------
136 | // open Fabulous.Attributes
137 |
138 | module Text =
139 | let Record = Attributes.defineBool "Record" TestUI_ViewUpdaters.updateRecord
140 |
141 | let Text =
142 | Attributes.defineSimpleScalarWithEquality "Text" TestUI_ViewUpdaters.updateText
143 |
144 |
145 | module TextStyle =
146 | let TextColor =
147 | Attributes.defineSimpleScalarWithEquality "TextColor" TestUI_ViewUpdaters.updateTextColor
148 |
149 | module Container =
150 | let Children =
151 | Attributes.defineListWidgetCollection "Container_Children" (fun target ->
152 | (target :?> IContainer).Children :> System.Collections.Generic.IList<_>)
153 |
154 | let Tap = defineContainerTappable "Container_Tap"
155 |
156 | module Button =
157 | let Pressed = definePressable "Button_Pressed"
158 | let Tap = defineTappable "Button_Tap"
159 | let Tap2 = defineTappable2 "Button_Tap2"
160 |
161 |
162 | module Automation =
163 | let AutomationId =
164 | Attributes.defineSimpleScalarWithEquality "AutomationId" TestUI_ViewUpdaters.updateAutomationId
165 |
166 | module NumericBag =
167 | let InlineValueOne =
168 | Attributes.defineSmallScalar "InlineValueOne" id TestUI_ViewUpdaters.updateNumericValueOne
169 |
170 | let InlineValueTwo =
171 | Attributes.defineSmallScalar "InlineValueTwo" id TestUI_ViewUpdaters.updateNumericValueTwo
172 |
173 | let InlineValueThree =
174 | Attributes.defineSmallScalar "InlineValueThree" BitConverter.UInt64BitsToDouble TestUI_ViewUpdaters.updateNumericValueThree
175 |
176 |
177 | let BoxedValueOne =
178 | Attributes.defineSimpleScalarWithEquality "BoxedValueOne" TestUI_ViewUpdaters.updateNumericValueOne
179 |
180 | let BoxedValueTwo =
181 | Attributes.defineSimpleScalarWithEquality "BoxedValueTwo" TestUI_ViewUpdaters.updateNumericValueTwo
182 |
183 | let BoxedValueThree =
184 | Attributes.defineSimpleScalarWithEquality "BoxedValueThree" TestUI_ViewUpdaters.updateNumericValueThree
185 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/Component.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 |
5 | /// This measure type is used to count the number of bindings in a component while building the computation expression
6 | []
7 | type binding
8 |
9 | type ComponentBody =
10 | delegate of EnvironmentContext * ViewTreeContext * ComponentContext -> struct (EnvironmentContext * ViewTreeContext * ComponentContext * Widget)
11 |
12 | []
13 | type ComponentData = { Key: string; Body: ComponentBody }
14 |
15 | type Component
16 | (componentDataKey: ScalarAttributeKey, envContext: EnvironmentContext, treeContext: ViewTreeContext, context: ComponentContext, body: ComponentBody) =
17 | let mutable _envContext = envContext
18 | let mutable _treeContext = treeContext
19 | let mutable _context = context
20 | let mutable _body = body
21 | let mutable _widget = Unchecked.defaultof<_>
22 | let mutable _view = null
23 | let mutable _contextSubscription: IDisposable = null
24 |
25 | let mutable _isReadyForRenderRequest = false
26 | let mutable _pendingRenderRequested = false
27 |
28 | member private this.MergeAttributes(rootWidget: Widget, componentWidgetOpt: Widget voption) =
29 | match componentWidgetOpt with
30 | | ValueNone ->
31 | struct (rootWidget.ScalarAttributes, rootWidget.WidgetAttributes, rootWidget.WidgetCollectionAttributes, rootWidget.EnvironmentAttributes)
32 |
33 | | ValueSome componentWidget ->
34 | let componentScalars =
35 | match componentWidget.ScalarAttributes with
36 | | [||] -> [||]
37 | | attrs ->
38 | let filteredAttrs =
39 | attrs |> Array.filter(fun scalarAttr -> scalarAttr.Key <> componentDataKey)
40 |
41 | filteredAttrs // skip the component data
42 |
43 | let scalars =
44 | match rootWidget.ScalarAttributes, componentScalars with
45 | | [||], [||] -> [||]
46 | | attrs, [||]
47 | | [||], attrs -> attrs
48 | | widgetAttrs, componentAttrs -> Array.append componentAttrs widgetAttrs
49 |
50 | let widgets =
51 | match rootWidget.WidgetAttributes, componentWidget.WidgetAttributes with
52 | | [||], [||] -> [||]
53 | | attrs, [||]
54 | | [||], attrs -> attrs
55 | | widgetAttrs, componentAttrs -> Array.append componentAttrs widgetAttrs
56 |
57 | let widgetColls =
58 | match rootWidget.WidgetCollectionAttributes, componentWidget.WidgetCollectionAttributes with
59 | | [||], [||] -> [||]
60 | | attrs, [||]
61 | | [||], attrs -> attrs
62 | | widgetAttrs, componentAttrs -> Array.append componentAttrs widgetAttrs
63 |
64 | let environments =
65 | match rootWidget.EnvironmentAttributes, componentWidget.EnvironmentAttributes with
66 | | [||], [||] -> [||]
67 | | attrs, [||]
68 | | [||], attrs -> attrs
69 | | widgetAttrs, componentAttrs -> Array.append componentAttrs widgetAttrs
70 |
71 | struct (scalars, widgets, widgetColls, environments)
72 |
73 | member this.CreateView(componentWidget: Widget voption) =
74 | _isReadyForRenderRequest <- false
75 | _contextSubscription <- _context.RenderNeeded.Subscribe(this.Render)
76 |
77 | let struct (envContext, treeContext, context, rootWidget) =
78 | _body.Invoke(_envContext, _treeContext, _context)
79 |
80 | _widget <- rootWidget
81 | _envContext <- envContext
82 | _treeContext <- treeContext
83 | _context <- context
84 |
85 | let struct (scalars, widgets, widgetColls, environments) =
86 | this.MergeAttributes(rootWidget, componentWidget)
87 |
88 | let rootWidget: Widget =
89 | { Key = rootWidget.Key
90 | #if DEBUG
91 | DebugName = rootWidget.DebugName
92 | #endif
93 | ScalarAttributes = scalars
94 | WidgetAttributes = widgets
95 | WidgetCollectionAttributes = widgetColls
96 | EnvironmentAttributes = environments }
97 |
98 | // Create the actual view
99 | let widgetDef = WidgetDefinitionStore.get rootWidget.Key
100 |
101 | let struct (node, view) =
102 | widgetDef.CreateView(rootWidget, envContext, treeContext, ValueNone)
103 |
104 | _view <- view
105 | _isReadyForRenderRequest <- true
106 |
107 | // ComponentContext.SetNeedsRender has been called before the view is created
108 | // We need to re-render the component now because the state has changed before we were ready
109 | if _pendingRenderRequested then
110 | _pendingRenderRequested <- false
111 | this.Render()
112 |
113 | struct (node, view)
114 |
115 | member this.AttachView(componentWidget: Widget, view: obj) =
116 | _isReadyForRenderRequest <- false
117 | _contextSubscription <- _context.RenderNeeded.Subscribe(this.Render)
118 |
119 | let struct (envContext, treeContext, context, rootWidget) =
120 | _body.Invoke(_envContext, _treeContext, _context)
121 |
122 | _widget <- rootWidget
123 | _envContext <- envContext
124 | _treeContext <- treeContext
125 | _context <- context
126 |
127 | let struct (scalars, widgets, widgetColls, environments) =
128 | this.MergeAttributes(rootWidget, ValueSome componentWidget)
129 |
130 | let rootWidget: Widget =
131 | { Key = rootWidget.Key
132 | #if DEBUG
133 | DebugName = rootWidget.DebugName
134 | #endif
135 | ScalarAttributes = scalars
136 | WidgetAttributes = widgets
137 | WidgetCollectionAttributes = widgetColls
138 | EnvironmentAttributes = environments }
139 |
140 | // Attach the widget to the existing view
141 | let widgetDef = WidgetDefinitionStore.get rootWidget.Key
142 |
143 | let node =
144 | widgetDef.AttachView(rootWidget, envContext, treeContext, ValueNone, view)
145 |
146 | _view <- view
147 | _isReadyForRenderRequest <- true
148 |
149 | // ComponentContext.SetNeedsRender has been called before the view is created
150 | // We need to re-render the component now because the state has changed before we were ready
151 | if _pendingRenderRequested then
152 | _pendingRenderRequested <- false
153 | this.Render()
154 |
155 | node
156 |
157 | member private this.RenderInternal() =
158 | if isNull _body then
159 | () // Component has been disposed
160 | else
161 | let prevRootWidget = _widget
162 | let prevContext = _context
163 |
164 | let struct (envContext, treeContext, context, currRootWidget) =
165 | _body.Invoke(_envContext, _treeContext, _context)
166 |
167 | _widget <- currRootWidget
168 | _envContext <- envContext
169 | _treeContext <- treeContext
170 |
171 | if prevContext <> context then
172 | _contextSubscription.Dispose()
173 | prevContext.Dispose()
174 | _contextSubscription <- context.RenderNeeded.Subscribe(this.Render)
175 | _context <- context
176 |
177 | let viewNode = treeContext.GetViewNode _view
178 |
179 | Reconciler.update treeContext.CanReuseView (ValueSome prevRootWidget) currRootWidget viewNode
180 |
181 | member this.Dispose() =
182 | if not(isNull _contextSubscription) then
183 | _contextSubscription.Dispose()
184 |
185 | if not(isNull _context) then
186 | _context.Dispose()
187 |
188 | _body <- null
189 | _widget <- Unchecked.defaultof<_>
190 | _view <- null
191 | _envContext <- Unchecked.defaultof<_>
192 | _treeContext <- Unchecked.defaultof<_>
193 | _contextSubscription <- null
194 | _context <- null
195 |
196 | interface IDisposable with
197 | member this.Dispose() = this.Dispose()
198 |
199 | member this.Render() =
200 | if isNull _body then
201 | () // Component has been disposed
202 | else if not _isReadyForRenderRequest then
203 | _pendingRenderRequested <- true
204 | else
205 | treeContext.SyncAction(this.RenderInternal)
206 |
--------------------------------------------------------------------------------
/src/Fabulous/ViewNode.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System
4 | open System.Collections.Generic
5 | open Fabulous
6 |
7 | /// Define the logic to apply diffs and store event handlers of its target control
8 | []
9 | type ViewNode =
10 | val mutable parent: IViewNode option
11 | val mutable envContext: EnvironmentContext
12 | val mutable treeContext: ViewTreeContext
13 | val mutable targetRef: WeakReference
14 | val mutable isDisposed: bool
15 | val mutable memoizedWidget: Widget option
16 | val mutable mapMsg: (obj -> obj) option
17 |
18 | // TODO consider combine handlers mapMsg and property bag
19 | // also we can probably use just Dictionary instead of Map because
20 | // ViewNode is supposed to be mutable, stateful and persistent object
21 | val handlers: Dictionary
22 |
23 | new(parent: IViewNode option, envContext: EnvironmentContext, treeContext: ViewTreeContext, target: WeakReference) =
24 | { parent = parent
25 | envContext = envContext
26 | treeContext = treeContext
27 | targetRef = target
28 | handlers = Dictionary()
29 | isDisposed = false
30 | memoizedWidget = None
31 | mapMsg = None }
32 |
33 | member inline private this.ApplyScalarDiffs(diffs: ScalarChanges inref) : unit =
34 | let node = this :> IViewNode
35 |
36 | for diff in diffs do
37 | match diff with
38 | | ScalarChange.Added added ->
39 | let key = added.Key
40 |
41 | match ScalarAttributeKey.getKind key with
42 | | ScalarAttributeKey.Inline ->
43 | let smallScalar = (AttributeDefinitionStore.getSmallScalar key)
44 |
45 | smallScalar.UpdateNode ValueNone (ValueSome added.NumericValue) node
46 |
47 | | ScalarAttributeKey.Boxed ->
48 | let scalar = (AttributeDefinitionStore.getScalar key)
49 |
50 | scalar.UpdateNode ValueNone (ValueSome added.Value) node
51 |
52 | | ScalarChange.Removed removed ->
53 |
54 | let key = removed.Key
55 |
56 | match ScalarAttributeKey.getKind key with
57 | | ScalarAttributeKey.Inline ->
58 | let smallScalar = (AttributeDefinitionStore.getSmallScalar key)
59 |
60 | smallScalar.UpdateNode (ValueSome removed.NumericValue) ValueNone node
61 |
62 | | ScalarAttributeKey.Boxed ->
63 | let scalar = (AttributeDefinitionStore.getScalar key)
64 |
65 | scalar.UpdateNode (ValueSome removed.Value) ValueNone node
66 |
67 | | ScalarChange.Updated(oldAttr, newAttr) ->
68 | let key = oldAttr.Key
69 |
70 | match ScalarAttributeKey.getKind key with
71 | | ScalarAttributeKey.Inline ->
72 | let smallScalar = (AttributeDefinitionStore.getSmallScalar key)
73 |
74 | smallScalar.UpdateNode (ValueSome oldAttr.NumericValue) (ValueSome newAttr.NumericValue) node
75 |
76 | | ScalarAttributeKey.Boxed ->
77 | let scalar = (AttributeDefinitionStore.getScalar key)
78 |
79 | scalar.UpdateNode (ValueSome oldAttr.Value) (ValueSome newAttr.Value) node
80 |
81 | member inline private this.ApplyWidgetDiffs(diffs: WidgetChanges inref) =
82 | for diff in diffs do
83 | match diff with
84 | | WidgetChange.Added newWidget ->
85 | let definition = (AttributeDefinitionStore.getWidget newWidget.Key)
86 |
87 | definition.UpdateNode ValueNone (ValueSome newWidget.Value) (this :> IViewNode)
88 |
89 | | WidgetChange.ReplacedBy(oldWidget, newWidget) ->
90 | let definition = (AttributeDefinitionStore.getWidget newWidget.Key)
91 |
92 | definition.UpdateNode (ValueSome oldWidget.Value) (ValueSome newWidget.Value) (this :> IViewNode)
93 |
94 | | WidgetChange.Removed removed ->
95 | let definition = (AttributeDefinitionStore.getWidget removed.Key)
96 |
97 | definition.UpdateNode (ValueSome removed.Value) ValueNone (this :> IViewNode)
98 |
99 | | WidgetChange.Updated(newAttr, diffs) ->
100 | let definition = (AttributeDefinitionStore.getWidget newAttr.Key)
101 |
102 | definition.ApplyDiff diffs (this :> IViewNode)
103 |
104 | member inline private this.ApplyWidgetCollectionDiffs(diffs: WidgetCollectionChanges inref) =
105 | for diff in diffs do
106 | match diff with
107 | | WidgetCollectionChange.Added added ->
108 | let definition = (AttributeDefinitionStore.getWidgetCollection added.Key)
109 |
110 | definition.UpdateNode ValueNone (ValueSome added.Value) (this :> IViewNode)
111 |
112 | | WidgetCollectionChange.Removed removed ->
113 | let definition = (AttributeDefinitionStore.getWidgetCollection removed.Key)
114 |
115 | definition.UpdateNode (ValueSome removed.Value) ValueNone (this :> IViewNode)
116 |
117 | | WidgetCollectionChange.Updated(oldAttr, newAttr, diffs) ->
118 | let definition = (AttributeDefinitionStore.getWidgetCollection newAttr.Key)
119 |
120 | definition.ApplyDiff oldAttr.Value diffs (this :> IViewNode)
121 |
122 | member inline private this.ApplyEnvironmentDiffs(diffs: EnvironmentChanges inref) : unit =
123 | let node = this :> IViewNode
124 |
125 | for diff in diffs do
126 | match diff with
127 | | EnvironmentChange.Added added ->
128 | let key = added.Key
129 | node.EnvironmentContext.SetInternal(key, added.Value, true)
130 |
131 | | EnvironmentChange.Removed removed ->
132 | let key = removed.Key
133 | node.EnvironmentContext.RemoveInternal(key, true)
134 |
135 | | EnvironmentChange.Updated(oldAttr, newAttr) ->
136 | let key = oldAttr.Key
137 | node.EnvironmentContext.SetInternal(key, newAttr.Value, true)
138 |
139 | interface IViewNode with
140 | member this.Target = this.targetRef.Target
141 | member this.TreeContext = this.treeContext
142 | member this.EnvironmentContext = this.envContext
143 |
144 | member this.MemoizedWidget
145 | with get () = this.memoizedWidget
146 | and set value = this.memoizedWidget <- value
147 |
148 | member this.Parent = this.parent
149 |
150 | member this.MapMsg
151 | with get () = this.mapMsg
152 | and set value = this.mapMsg <- value
153 |
154 | member this.IsDisconnected = this.isDisposed
155 |
156 | member this.TryGetHandler(key: string) =
157 | match this.handlers.TryGetValue(key) with
158 | | false, _ -> ValueNone
159 | | true, handler -> ValueSome(handler)
160 |
161 | member this.SetHandler(key: string, handler: IDisposable) = this.handlers[key] <- handler
162 |
163 | member this.RemoveHandler(key: string) =
164 | if this.handlers.ContainsKey(key) then
165 | let handler = this.handlers[key]
166 | this.handlers.Remove(key) |> ignore
167 | handler.Dispose()
168 |
169 | member this.Dispose() =
170 | this.isDisposed <- true
171 |
172 | // Dispose all the event handlers of this node
173 | for kvp in this.handlers do
174 | kvp.Value.Dispose()
175 |
176 | this.handlers.Clear()
177 |
178 | // Dispose the attached Component if any
179 | if this.targetRef.IsAlive then
180 | let comp = this.treeContext.GetComponent(this.targetRef.Target) :?> IDisposable
181 |
182 | if not(isNull comp) then
183 | comp.Dispose()
184 | this.treeContext.SetComponent null this.targetRef.Target
185 |
186 | this.parent <- None
187 | this.treeContext <- Unchecked.defaultof<_>
188 | this.targetRef <- null
189 |
190 | member this.ApplyDiff(diff) =
191 | if not this.targetRef.IsAlive then
192 | ()
193 | else
194 | this.ApplyEnvironmentDiffs(&diff.EnvironmentChanges)
195 | this.ApplyWidgetDiffs(&diff.WidgetChanges)
196 | this.ApplyWidgetCollectionDiffs(&diff.WidgetCollectionChanges)
197 | this.ApplyScalarDiffs(&diff.ScalarChanges)
198 |
--------------------------------------------------------------------------------
/src/Fabulous/Components/README.md:
--------------------------------------------------------------------------------
1 | ## What's going on here:
2 | This is an attempt at making re-executable computation expressions with a context being passed implicitly.
3 |
4 | ## History and constraints
5 | Today in Fabulous, there is only one source of truth for the whole app: it's root state.
6 |
7 | Whenever a change happens in this root state, the whole view hierarchy is re-evaluated to check for any
8 | UI update that needs to be applied on the screen. Having this single source of truth is great to ensure consistency,
9 | but it implies a lot of unnecessary processing because 99% of the time a state change will only have an impact locally,
10 | not globally, hence it would be better to only re-evaluate the local view hierarchy.
11 |
12 | This idea is known as "components": you can see them as some kind of mini-apps managing their own local state
13 | that can trigger re-evaluation on their own and that can be composed together to make an actual Fabulous application.
14 |
15 | Despite quite a lot of prior arts (SwiftUI "View" protocol, React components, FuncUI components, Vide builders, etc.),
16 | it has been difficult to come up with a component approach in Fabulous due to the unique set of constraints: mobile & F#.
17 | While the implementation is straightforward in the other F# libraries (FuncUI, Vide), they make heavy use of closures
18 | which allocate of lot of memory; something Fabulous cannot afford because GC would keep freezing the app
19 | on lower end Android smartphones due to limited memory. Hence it is better to avoid closures and make heavy use
20 | of structs instead of classes.
21 |
22 | Also another aspect why it has been difficult to come up with anything is the opinionated ergonomics wanted for Fabulous.
23 | Fabulous took a similar approach to SwiftUI: a builder pattern with handcrafted widgets and modifiers.
24 | But contrary to Swift, in .NET (C# & F#) using interfaces (protocols in Swift) over struct will result in boxing because
25 | a struct first need to be transformed into an object before being casted to the interface. This triggers a lot of memory
26 | allocation, which is what we want to avoid in the first place with the structs, so a different approach is required.
27 |
28 | type IComponent = interface end
29 | type [] TextWidget(value: string) = interface IComponent
30 |
31 | let text = TextWidget("Hello")
32 | let component = text :> IComponent // ----> let component = text >> box :> IComponent
33 |
34 | Another point we want to take a look into is the ability to use any kind of state management, not only MVU.
35 |
36 | With all those constraints in place, we want something that can easily be composed into Fabulous 2 DSL ergonomics,
37 | lets you choose your own state management, and almost allocation-free to be friendly with low end mobile devices.
38 |
39 | This means we need to make heavy use of inlining and structs.
40 | Computation expressions to the rescue.
41 |
42 | ## Implementation ideas
43 |
44 | A component needs to somehow hold its own state and have a view description that can be evaluated at will everytime
45 | the state changes.
46 |
47 | let component =
48 | view {
49 | let! count = state 0
50 |
51 | VStack() {
52 | Text($"Count is {count.Value}")
53 | Button("Increment", fun () -> count.Set(count + 1))
54 | Button("Decrement", fun () -> count.Set(count - 1))
55 | }
56 | }
57 |
58 | To achieve this, we can create a ViewBuilder computation expression that will store its body into a function.
59 | The state is bound to variables by using `let!`.
60 |
61 |
62 | builder.Run(
63 | builder.Delay(
64 | builder.Bind(state 0, fun count -> // this is for "let! count = state 0"
65 | builder.Yield( // this is an implicit yield
66 | VStack() { ... }
67 | )
68 | )
69 | )
70 | )
71 |
72 | The ViewBuilder makes use of the implicit yield capability of F# by implementing: "Yield", "Combine", and "Delay".
73 | Contrary to what the F# documentation states, "Zero" is not required to have implicit yield.
74 |
75 | - Yield: Widget -> Contextual
76 | - Combine: [] Contextual * [] Contextual -> Contextual
77 | - Delay: [] (unit -> Contextual) -> Contextual
78 |
79 | Contextual is a composable delegate that take a Context (so we can pass it implicitly around the CE, mainly to be used
80 | in "Bind" without making it visible in the user code) and return a Widget, which is the typical body of a component.
81 |
82 | Why are we using a delegate here?
83 | Delegates are basically lambdas, so combining this with inlined CE methods ("member inline Yield", etc.) and the attribute
84 | [], we can flatten the whole body of the CE into a single Contextual lambda.
85 |
86 | Example:
87 |
88 | let result =
89 | (fun () -> // the Delay
90 | Contextual(fun ctx -> // the Bind
91 | let count = ctx.GetState(0)
92 | Contextual(fun ctx -> // the Yield
93 | VStack() {
94 | Text($"Count is {count.Value}")
95 | }
96 | )
97 | )
98 | )()
99 |
100 | will become
101 |
102 | let result =
103 | Contextual(fun ctx ->
104 | let count = ctx.GetState(0)
105 | VStack() {
106 | Text($"Count is {count.Value}")
107 | }
108 | )
109 |
110 |
111 | Since we already get a "Contextual" at every step of the CE, "Run" doesn't need any specific implementation except
112 | returning the latest Contextual function.
113 |
114 | ## How does state works and how everything gets re-evaluated on change
115 |
116 | "let! count = state 0" is a request to the implicit context passed around in the CE to retrieve the previous state value
117 | or initialize it with the default value "0"
118 |
119 | inline state 0 // helper function to hide the default factory lambda
120 | --> StateRequest(fun () -> 0) // StateRequest is also an inlinable delegate
121 | --> let! === ctx.TryGetValue() or ctx.SetValue(0)
122 | --> struct State(ctx, key, value)
123 |
124 | - static member inline Bind(_: ViewBuilder, [] request: StateRequest<'T>, [] continuation: State<'T> -> Contextual)
125 |
126 | Since we are passing the Context itself into the State struct value given to the user, when the user calls "count.Set(newValue)",
127 | it will mark the context as dirty, meaning a re-evaluation is needed.
128 |
129 | This context is originated from the Component that hold both its own Context and the Contextual lambda created by the CE.
130 | The Component listens to its context Dirtied event to know when to re-evaluate the body.
131 |
132 | Another important point is, the context uses positional indexes to store and retrieve the states.
133 |
134 | Say you have the following body:
135 |
136 | let body =
137 | view {
138 | let! firstName = state "George"
139 | let! lastName = state "Roger"
140 | (...)
141 | }
142 |
143 | behind the scene, when calling Bind for firstName, Context will switch to the index 0.
144 | Then when calling Bind for lastName, Context will switch to index 1.
145 | Resulting in
146 |
147 | Context.values =
148 | [0] = "George"
149 | [1] = "Roger"
150 |
151 | On subsequent reevaluations, Context switch back to index 0 to retrieve the values in order
152 | let firstName = Context.values[0]
153 | let lastName = Context.values[1]
154 |
155 | This means conditional state is to be avoided
156 |
157 | DONT:
158 | let body =
159 | view {
160 | if someCondition then
161 | let! firstName = state "George"
162 | Text("First name is {firstName.Value}")
163 | else
164 | let! lastName = state "Roger"
165 | Text("Last name is {lastName.Value}")
166 | }
167 |
168 | This will returning a confusing result.
169 |
170 | // 1st execution - someCondition = true
171 | First name is George
172 |
173 | // 2nd execution - someCondition = false
174 | Last name is George
175 |
176 | As a user, you expect to have two independent states: one for FirstName, one for LastName.
177 | But since Context uses positional access to retrieve the values, firstName and lastName make no difference for Context
178 | since they will both have Position = 0.
179 |
180 |
181 | ###############
182 |
183 | A nice thing about this approach is that we can share a context between several components.
184 | This is useful is the context of controls repeated several times that actually represent a same thing
185 | (eg the avatar picture in the chat message page that gets repeated in front of each message)
186 |
187 | let sharedContext = Context()
188 |
189 | let avatar1 = Component(sharedContext, Avatar())
190 | let avatar2 = Component(sharedContext, Avatar())
191 |
192 | avatar1.Background <- Blue
193 | // Automatically triggers avatar2.Background to become Blue
--------------------------------------------------------------------------------
/src/Fabulous/AttributeDefinitions.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | module ScalarAttributeDefinitions =
4 | /// A small scalar attribute.
5 | /// When we can encode the value as a uint64 (64 bits), we should prefer this type.
6 | /// The value will be kept on the stack avoiding GC pressure.
7 | []
8 | type SmallScalarAttributeData =
9 | { UpdateNode: uint64 voption -> uint64 voption -> IViewNode -> unit }
10 |
11 | /// A regular scalar attribute.
12 | /// The value will be boxed and put on the heap, which can trigger GC to pass.
13 | /// Prefer small scalar attribute when possible.
14 | []
15 | type ScalarAttributeData =
16 | { UpdateNode: obj voption -> obj voption -> IViewNode -> unit
17 | CompareBoxed: obj -> obj -> ScalarAttributeComparison }
18 |
19 | /// Attribute definition for small scalar properties (encodable on 64 bits)
20 | []
21 | type SmallScalarAttributeDefinition<'T> =
22 | { Key: ScalarAttributeKey
23 | Name: string }
24 |
25 | member inline x.WithValue(value: 'T, [] encode: 'T -> uint64) : ScalarAttribute =
26 | { Key = x.Key
27 | #if DEBUG
28 | DebugName = x.Name
29 | #endif
30 | NumericValue = encode(value)
31 | Value = null }
32 |
33 | static member inline CreateAttributeData<'T>
34 | ([] decode: uint64 -> 'T, [] updateNode: 'T voption -> 'T voption -> IViewNode -> unit)
35 | : SmallScalarAttributeData =
36 | { UpdateNode =
37 | (fun oldValueOpt newValueOpt node ->
38 | let oldValueOpt =
39 | match oldValueOpt with
40 | | ValueNone -> ValueNone
41 | | ValueSome v -> ValueSome(decode(v))
42 |
43 | let newValueOpt =
44 | match newValueOpt with
45 | | ValueNone -> ValueNone
46 | | ValueSome v -> ValueSome(decode(v))
47 |
48 | updateNode oldValueOpt newValueOpt node) }
49 |
50 | /// Attribute definition for boxed scalar properties
51 | []
52 | type SimpleScalarAttributeDefinition<'T> =
53 | { Key: ScalarAttributeKey
54 | Name: string }
55 |
56 | member inline x.WithValue(value: 'T) : ScalarAttribute =
57 | { Key = x.Key
58 | #if DEBUG
59 | DebugName = x.Name
60 | #endif
61 | NumericValue = 0UL
62 | Value = value }
63 |
64 | static member CreateAttributeData
65 | (compare: 'T -> 'T -> ScalarAttributeComparison, updateNode: 'T voption -> 'T voption -> IViewNode -> unit)
66 | : ScalarAttributeData =
67 | { CompareBoxed = (fun a b -> compare (unbox<'T> a) (unbox<'T> b))
68 | UpdateNode =
69 | (fun oldValueOpt newValueOpt node ->
70 | let oldValueOpt =
71 | match oldValueOpt with
72 | | ValueNone -> ValueNone
73 | | ValueSome v -> ValueSome(unbox<'T> v)
74 |
75 | let newValueOpt =
76 | match newValueOpt with
77 | | ValueNone -> ValueNone
78 | | ValueSome v -> ValueSome(unbox<'T> v)
79 |
80 | updateNode oldValueOpt newValueOpt node) }
81 |
82 | /// Attribute definition for boxed scalar properties with a custom conversion before being applied to the view
83 | []
84 | type ScalarAttributeDefinition<'modelType, 'valueType> =
85 | { Key: ScalarAttributeKey
86 | Name: string }
87 |
88 | member inline x.WithValue(value: 'modelType) : ScalarAttribute =
89 | { Key = x.Key
90 | #if DEBUG
91 | DebugName = x.Name
92 | #endif
93 | NumericValue = 0UL
94 | Value = value }
95 |
96 | static member inline CreateAttributeData<'modelType, 'valueType>
97 | (
98 | [] convertValue: 'modelType -> 'valueType,
99 | [] compare: 'modelType -> 'modelType -> ScalarAttributeComparison,
100 | [] updateNode: 'valueType voption -> 'valueType voption -> IViewNode -> unit
101 | ) : ScalarAttributeData =
102 | { CompareBoxed = (fun a b -> compare (unbox<'modelType> a) (unbox<'modelType> b))
103 | UpdateNode =
104 | (fun oldValueOpt newValueOpt node ->
105 | let oldValueOpt =
106 | match oldValueOpt with
107 | | ValueNone -> ValueNone
108 | | ValueSome v -> ValueSome(convertValue(unbox<'modelType> v))
109 |
110 | let newValueOpt =
111 | match newValueOpt with
112 | | ValueNone -> ValueNone
113 | | ValueSome v -> ValueSome(convertValue(unbox<'modelType> v))
114 |
115 | updateNode oldValueOpt newValueOpt node) }
116 |
117 | module WidgetAttributeDefinitions =
118 | []
119 | type WidgetAttributeData =
120 | { ApplyDiff: WidgetDiff -> IViewNode -> unit
121 | UpdateNode: Widget voption -> Widget voption -> IViewNode -> unit }
122 |
123 | /// Attribute definition for widget properties
124 | []
125 | type WidgetAttributeDefinition =
126 | { Key: WidgetAttributeKey
127 | Name: string }
128 |
129 | member inline x.WithValue(value: Widget) : WidgetAttribute =
130 | { Key = x.Key
131 | #if DEBUG
132 | DebugName = x.Name
133 | #endif
134 | Value = value }
135 |
136 | module WidgetCollectionAttributeDefinitions =
137 | []
138 | type WidgetCollectionAttributeData =
139 | { ApplyDiff: ArraySlice -> WidgetCollectionItemChanges -> IViewNode -> unit
140 | UpdateNode: ArraySlice voption -> ArraySlice voption -> IViewNode -> unit }
141 |
142 | /// Attribute definition for collection properties
143 | []
144 | type WidgetCollectionAttributeDefinition =
145 | { Key: WidgetCollectionAttributeKey
146 | Name: string }
147 |
148 | member inline x.WithValue(value: ArraySlice) : WidgetCollectionAttribute =
149 | { Key = x.Key
150 | #if DEBUG
151 | DebugName = x.Name
152 | #endif
153 | Value = value }
154 |
155 | module AttributeDefinitionStore =
156 | open ScalarAttributeDefinitions
157 | open WidgetAttributeDefinitions
158 | open WidgetCollectionAttributeDefinitions
159 |
160 | let private _scalars = ResizeArray()
161 | let private _smallScalars = ResizeArray()
162 | let private _widgets = ResizeArray()
163 | let private _widgetCollections = ResizeArray()
164 |
165 | let registerSmallScalar (data: SmallScalarAttributeData) : ScalarAttributeKey =
166 | let index = _smallScalars.Count
167 | _smallScalars.Add(data)
168 |
169 | (index ||| ScalarAttributeKey.Code.Inline) * 1
170 |
171 | let registerScalar (data: ScalarAttributeData) : ScalarAttributeKey =
172 | let index = _scalars.Count
173 | _scalars.Add(data)
174 |
175 | (index ||| ScalarAttributeKey.Code.Boxed) * 1
176 |
177 | let registerWidget (data: WidgetAttributeData) : WidgetAttributeKey =
178 | let index = _widgets.Count
179 | _widgets.Add(data)
180 | index * 1
181 |
182 | let registerWidgetCollection (data: WidgetCollectionAttributeData) : WidgetCollectionAttributeKey =
183 | let index = _widgetCollections.Count
184 | _widgetCollections.Add(data)
185 | index * 1
186 |
187 | let getScalar (key: ScalarAttributeKey) : ScalarAttributeData =
188 | let index = ScalarAttributeKey.getKeyValue key
189 | _scalars[index]
190 |
191 | let getSmallScalar (key: ScalarAttributeKey) : SmallScalarAttributeData =
192 | let index = ScalarAttributeKey.getKeyValue key
193 | _smallScalars[index]
194 |
195 | let getWidget (key: WidgetAttributeKey) : WidgetAttributeData = _widgets[int key]
196 |
197 | let getWidgetCollection (key: WidgetCollectionAttributeKey) : WidgetCollectionAttributeData = _widgetCollections[int key]
198 |
199 | module AttributeHelpers =
200 | open ScalarAttributeDefinitions
201 |
202 | let tryFindSimpleScalarAttribute (definition: SimpleScalarAttributeDefinition<'T>) (widget: Widget) =
203 | match widget.ScalarAttributes |> Array.tryFind(fun attr -> attr.Key = definition.Key) with
204 | | None -> ValueNone
205 | | Some attr -> ValueSome(unbox<'T> attr.Value)
206 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.tlog
97 | *.vspscc
98 | *.vssscc
99 | .builds
100 | *.pidb
101 | *.svclog
102 | *.scc
103 |
104 | # Chutzpah Test files
105 | _Chutzpah*
106 |
107 | # Visual C++ cache files
108 | ipch/
109 | *.aps
110 | *.ncb
111 | *.opendb
112 | *.opensdf
113 | *.sdf
114 | *.cachefile
115 | *.VC.db
116 | *.VC.VC.opendb
117 |
118 | # Visual Studio profiler
119 | *.psess
120 | *.vsp
121 | *.vspx
122 | *.sap
123 |
124 | # Visual Studio Trace Files
125 | *.e2e
126 |
127 | # TFS 2012 Local Workspace
128 | $tf/
129 |
130 | # Guidance Automation Toolkit
131 | *.gpState
132 |
133 | # ReSharper is a .NET coding add-in
134 | _ReSharper*/
135 | *.[Rr]e[Ss]harper
136 | *.DotSettings.user
137 |
138 | # TeamCity is a build add-in
139 | _TeamCity*
140 |
141 | # DotCover is a Code Coverage Tool
142 | *.dotCover
143 |
144 | # AxoCover is a Code Coverage Tool
145 | .axoCover/*
146 | !.axoCover/settings.json
147 |
148 | # Coverlet is a free, cross platform Code Coverage Tool
149 | coverage*.json
150 | coverage*.xml
151 | coverage*.info
152 |
153 | # Visual Studio code coverage results
154 | *.coverage
155 | *.coveragexml
156 |
157 | # NCrunch
158 | _NCrunch_*
159 | .*crunch*.local.xml
160 | nCrunchTemp_*
161 |
162 | # MightyMoose
163 | *.mm.*
164 | AutoTest.Net/
165 |
166 | # Web workbench (sass)
167 | .sass-cache/
168 |
169 | # Installshield output folder
170 | [Ee]xpress/
171 |
172 | # DocProject is a documentation generator add-in
173 | DocProject/buildhelp/
174 | DocProject/Help/*.HxT
175 | DocProject/Help/*.HxC
176 | DocProject/Help/*.hhc
177 | DocProject/Help/*.hhk
178 | DocProject/Help/*.hhp
179 | DocProject/Help/Html2
180 | DocProject/Help/html
181 |
182 | # Click-Once directory
183 | publish/
184 |
185 | # Publish Web Output
186 | *.[Pp]ublish.xml
187 | *.azurePubxml
188 | # Note: Comment the next line if you want to checkin your web deploy settings,
189 | # but database connection strings (with potential passwords) will be unencrypted
190 | *.pubxml
191 | *.publishproj
192 |
193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
194 | # checkin your Azure Web App publish settings, but sensitive information contained
195 | # in these scripts will be unencrypted
196 | PublishScripts/
197 |
198 | # NuGet Packages
199 | *.nupkg
200 | # NuGet Symbol Packages
201 | *.snupkg
202 | # The packages folder can be ignored because of Package Restore
203 | **/[Pp]ackages/*
204 | # except build/, which is used as an MSBuild target.
205 | !**/[Pp]ackages/build/
206 | # Uncomment if necessary however generally it will be regenerated when needed
207 | #!**/[Pp]ackages/repositories.config
208 | # NuGet v3's project.json files produces more ignorable files
209 | *.nuget.props
210 | *.nuget.targets
211 |
212 | # Microsoft Azure Build Output
213 | csx/
214 | *.build.csdef
215 |
216 | # Microsoft Azure Emulator
217 | ecf/
218 | rcf/
219 |
220 | # Windows Store app package directories and files
221 | AppPackages/
222 | BundleArtifacts/
223 | Package.StoreAssociation.xml
224 | _pkginfo.txt
225 | *.appx
226 | *.appxbundle
227 | *.appxupload
228 |
229 | # Visual Studio cache files
230 | # files ending in .cache can be ignored
231 | *.[Cc]ache
232 | # but keep track of directories ending in .cache
233 | !?*.[Cc]ache/
234 |
235 | # Others
236 | ClientBin/
237 | ~$*
238 | *~
239 | *.dbmdl
240 | *.dbproj.schemaview
241 | *.jfm
242 | *.pfx
243 | *.publishsettings
244 | orleans.codegen.cs
245 |
246 | # Including strong name files can present a security risk
247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
248 | #*.snk
249 |
250 | # Since there are multiple workflows, uncomment next line to ignore bower_components
251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
252 | #bower_components/
253 |
254 | # RIA/Silverlight projects
255 | Generated_Code/
256 |
257 | # Backup & report files from converting an old project file
258 | # to a newer Visual Studio version. Backup files are not needed,
259 | # because we have git ;-)
260 | _UpgradeReport_Files/
261 | Backup*/
262 | UpgradeLog*.XML
263 | UpgradeLog*.htm
264 | ServiceFabricBackup/
265 | *.rptproj.bak
266 |
267 | # SQL Server files
268 | *.mdf
269 | *.ldf
270 | *.ndf
271 |
272 | # Business Intelligence projects
273 | *.rdl.data
274 | *.bim.layout
275 | *.bim_*.settings
276 | *.rptproj.rsuser
277 | *- [Bb]ackup.rdl
278 | *- [Bb]ackup ([0-9]).rdl
279 | *- [Bb]ackup ([0-9][0-9]).rdl
280 |
281 | # Microsoft Fakes
282 | FakesAssemblies/
283 |
284 | # GhostDoc plugin setting file
285 | *.GhostDoc.xml
286 |
287 | # Node.js Tools for Visual Studio
288 | .ntvs_analysis.dat
289 | node_modules/
290 |
291 | # Visual Studio 6 build log
292 | *.plg
293 |
294 | # Visual Studio 6 workspace options file
295 | *.opt
296 |
297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
298 | *.vbw
299 |
300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
301 | *.vbp
302 |
303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
304 | *.dsw
305 | *.dsp
306 |
307 | # Visual Studio 6 technical files
308 | *.ncb
309 | *.aps
310 |
311 | # Visual Studio LightSwitch build output
312 | **/*.HTMLClient/GeneratedArtifacts
313 | **/*.DesktopClient/GeneratedArtifacts
314 | **/*.DesktopClient/ModelManifest.xml
315 | **/*.Server/GeneratedArtifacts
316 | **/*.Server/ModelManifest.xml
317 | _Pvt_Extensions
318 |
319 | # Paket dependency manager
320 | .paket/paket.exe
321 | paket-files/
322 |
323 | # FAKE - F# Make
324 | .fake/
325 |
326 | # CodeRush personal settings
327 | .cr/personal
328 |
329 | # Python Tools for Visual Studio (PTVS)
330 | __pycache__/
331 | *.pyc
332 |
333 | # Cake - Uncomment if you are using it
334 | # tools/**
335 | # !tools/packages.config
336 |
337 | # Tabs Studio
338 | *.tss
339 |
340 | # Telerik's JustMock configuration file
341 | *.jmconfig
342 |
343 | # BizTalk build output
344 | *.btp.cs
345 | *.btm.cs
346 | *.odx.cs
347 | *.xsd.cs
348 |
349 | # OpenCover UI analysis results
350 | OpenCover/
351 |
352 | # Azure Stream Analytics local run output
353 | ASALocalRun/
354 |
355 | # MSBuild Binary and Structured Log
356 | *.binlog
357 |
358 | # NVidia Nsight GPU debugger configuration file
359 | *.nvuser
360 |
361 | # MFractors (Xamarin productivity tool) working folder
362 | .mfractor/
363 |
364 | # Local History for Visual Studio
365 | .localhistory/
366 |
367 | # Visual Studio History (VSHistory) files
368 | .vshistory/
369 |
370 | # BeatPulse healthcheck temp database
371 | healthchecksdb
372 |
373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
374 | MigrationBackup/
375 |
376 | # Ionide (cross platform F# VS Code tools) working folder
377 | .ionide/
378 |
379 | # Fody - auto-generated XML schema
380 | FodyWeavers.xsd
381 |
382 | # VS Code files for those working on multiple tools
383 | .vscode/*
384 | !.vscode/settings.json
385 | !.vscode/tasks.json
386 | !.vscode/launch.json
387 | !.vscode/extensions.json
388 | *.code-workspace
389 |
390 | # Local History for Visual Studio Code
391 | .history/
392 |
393 | # Windows Installer files from build outputs
394 | *.cab
395 | *.msi
396 | *.msix
397 | *.msm
398 | *.msp
399 |
400 | # JetBrains Rider
401 | *.sln.iml
402 |
403 | ##
404 | ## Visual studio for Mac
405 | ##
406 |
407 |
408 | # globs
409 | Makefile.in
410 | *.userprefs
411 | *.usertasks
412 | config.make
413 | config.status
414 | aclocal.m4
415 | install-sh
416 | autom4te.cache/
417 | *.tar.gz
418 | tarballs/
419 | test-results/
420 |
421 | # Mac bundle stuff
422 | *.dmg
423 | *.app
424 |
425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
426 | # General
427 | .DS_Store
428 | .AppleDouble
429 | .LSOverride
430 |
431 | # Icon must end with two \r
432 | Icon
433 |
434 |
435 | # Thumbnails
436 | ._*
437 |
438 | # Files that might appear in the root of a volume
439 | .DocumentRevisions-V100
440 | .fseventsd
441 | .Spotlight-V100
442 | .TemporaryItems
443 | .Trashes
444 | .VolumeIcon.icns
445 | .com.apple.timemachine.donotpresent
446 |
447 | # Directories potentially created on remote AFP share
448 | .AppleDB
449 | .AppleDesktop
450 | Network Trash Folder
451 | Temporary Items
452 | .apdisk
453 |
454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
455 | # Windows thumbnail cache files
456 | Thumbs.db
457 | ehthumbs.db
458 | ehthumbs_vista.db
459 |
460 | # Dump file
461 | *.stackdump
462 |
463 | # Folder config file
464 | [Dd]esktop.ini
465 |
466 | # Recycle Bin used on file shares
467 | $RECYCLE.BIN/
468 |
469 | # Windows Installer files
470 | *.cab
471 | *.msi
472 | *.msix
473 | *.msm
474 | *.msp
475 |
476 | # Windows shortcuts
477 | *.lnk
478 |
479 | .idea
480 |
481 | Resource.designer.cs
482 | nupkgs/
--------------------------------------------------------------------------------
/src/Fabulous.Tests/APISketchTests/TestUI.Widgets.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous.Tests.APISketchTests
2 |
3 | module TestUI_Widgets =
4 |
5 | open System
6 | open System.Runtime.CompilerServices
7 | open Fabulous
8 | open Fabulous.StackAllocatedCollections
9 | open Fabulous.StackAllocatedCollections.StackList
10 | open Platform
11 | open TestUI_Attributes
12 | open TestUI_ViewNode
13 | open TestUI_Component
14 |
15 |
16 | //----WidgetsBuilderCE---
17 |
18 |
19 |
20 |
21 | //-------Widgets
22 |
23 | module Widgets =
24 | let register<'T when 'T :> TestViewElement and 'T: (new: unit -> 'T)> () =
25 | let key = WidgetDefinitionStore.getNextKey()
26 |
27 | let definition =
28 | { Key = key
29 | Name = typeof<'T>.Name
30 | TargetType = typeof<'T>
31 | CreateView =
32 | fun (widget, envContext, treeContext, parentNode) ->
33 | // let name = typeof<'T>.Name
34 | // printfn $"Creating view for {name}"
35 |
36 | let view = new 'T()
37 | let weakReference = WeakReference(view)
38 |
39 | let parentNode =
40 | match parentNode with
41 | | ValueNone -> None
42 | | ValueSome parent -> Some parent
43 |
44 | let viewNode = new ViewNode(parentNode, envContext, treeContext, weakReference)
45 |
46 | view.PropertyBag.Add(ViewNode.ViewNodeProperty, viewNode)
47 |
48 | let oldWidget: Widget voption = ValueNone
49 |
50 | Reconciler.update treeContext.CanReuseView oldWidget widget viewNode
51 | struct (viewNode :> IViewNode, box view)
52 | AttachView = fun (_widget, _envContext, _treeContext, _parentNode, _view) -> failwith "not implemented" }
53 |
54 | WidgetDefinitionStore.set key definition
55 | key
56 |
57 |
58 |
59 |
60 |
61 | //-----MARKERS-----------
62 | type IMarker = interface end
63 |
64 | type TextMarker =
65 | inherit IMarker
66 |
67 | type TestLabelMarker =
68 | inherit TextMarker
69 |
70 | type TestButtonMarker =
71 | inherit TextMarker
72 |
73 | type TestStackMarker =
74 | inherit IMarker
75 |
76 | type TestNumericBagMarker =
77 | inherit IMarker
78 |
79 | //----------------
80 |
81 | /// ------------ Extensions
82 | []
83 | type WidgetExtensions() =
84 | []
85 | static member inline automationId<'msg, 'marker when 'msg: equality and 'marker :> IMarker>(this: WidgetBuilder<'msg, 'marker>, value: string) =
86 | this.AddScalar(Attributes.Automation.AutomationId.WithValue(value))
87 |
88 | []
89 | static member inline automationId<'msg, 'marker, 'itemMarker when 'msg: equality and 'marker :> IMarker>
90 | (this: CollectionBuilder<'msg, 'marker, 'itemMarker>, value: string)
91 | =
92 | this.AddScalar(Attributes.Automation.AutomationId.WithValue(value))
93 |
94 | []
95 | static member inline textColor<'msg, 'marker when 'msg: equality and 'marker :> TextMarker>(this: WidgetBuilder<'msg, 'marker>, value: string) =
96 | this.AddScalar(Attributes.TextStyle.TextColor.WithValue(value))
97 |
98 |
99 | []
100 | static member inline record<'msg, 'marker when 'msg: equality and 'marker :> TestLabelMarker>(this: WidgetBuilder<'msg, 'marker>, value: bool) =
101 | this.AddScalar(Attributes.Text.Record.WithValue(value))
102 |
103 |
104 | []
105 | static member inline tap<'msg, 'marker when 'msg: equality and 'marker :> TestButtonMarker>(this: WidgetBuilder<'msg, 'marker>, value: 'msg) =
106 | this.AddScalar(Attributes.Button.Tap.WithValue(value))
107 |
108 |
109 | []
110 | static member inline tap2<'msg, 'marker when 'msg: equality and 'marker :> TestButtonMarker>(this: WidgetBuilder<'msg, 'marker>, value: 'msg) =
111 | this.AddScalar(Attributes.Button.Tap2.WithValue(value))
112 |
113 |
114 | []
115 | static member inline tapContainer<'msg, 'marker when 'msg: equality and 'marker :> TestStackMarker>(this: WidgetBuilder<'msg, 'marker>, value: 'msg) =
116 | this.AddScalar(Attributes.Container.Tap.WithValue(value))
117 |
118 | ///----------------
119 | ///----------------
120 |
121 | []
122 | type View private () =
123 | static let TestLabelKey = Widgets.register()
124 | static let TestButtonKey = Widgets.register()
125 | static let TestStackKey = Widgets.register()
126 | static let TestNumericBagKey = Widgets.register()
127 |
128 | static member Label(text: string) =
129 | WidgetBuilder<'msg, TestLabelMarker>(TestLabelKey, Attributes.Text.Text.WithValue(text))
130 |
131 |
132 | static member Button(text: string, onClicked: 'msg) =
133 | WidgetBuilder<'msg, TestButtonMarker>(TestButtonKey, Attributes.Text.Text.WithValue(text), Attributes.Button.Pressed.WithValue(onClicked))
134 |
135 | static member BoxedNumericBag(one, two, three) =
136 | WidgetBuilder<'msg, TestNumericBagMarker>(
137 | TestNumericBagKey,
138 | Attributes.NumericBag.BoxedValueOne.WithValue(one),
139 | Attributes.NumericBag.BoxedValueTwo.WithValue(two),
140 | Attributes.NumericBag.BoxedValueThree.WithValue(three)
141 | )
142 |
143 | static member InlineNumericBag(one, two, three) =
144 | WidgetBuilder<'msg, TestNumericBagMarker>(
145 | TestNumericBagKey,
146 | Attributes.NumericBag.InlineValueOne.WithValue(one, (fun x -> x)),
147 | Attributes.NumericBag.InlineValueTwo.WithValue(two, (fun x -> x)),
148 | // Attributes.NumericBag.InlineValueOne.WithValue(one, Attributes.func),
149 | // Attributes.NumericBag.InlineValueTwo.WithValue(two, Attributes.func),
150 | Attributes.NumericBag.InlineValueThree.WithValue(three, BitConverter.DoubleToUInt64Bits)
151 | )
152 |
153 | static member Stack<'msg, 'marker when 'msg: equality and 'marker :> IMarker>() =
154 | CollectionBuilder<'msg, TestStackMarker, 'marker>(TestStackKey, StackList.empty(), Attributes.Container.Children)
155 |
156 | []
157 | type CollectionBuilderExtensions =
158 | []
159 | static member inline Yield<'msg, 'marker, 'itemMarker when 'msg: equality and 'itemMarker :> IMarker>
160 | (_: CollectionBuilder<'msg, 'marker, IMarker>, x: WidgetBuilder<'msg, 'itemMarker>)
161 | : Content<'msg> =
162 | CollectionBuilder.yieldImpl x
163 |
164 | []
165 | static member inline Yield<'msg, 'marker, 'itemMarker when 'msg: equality and 'itemMarker :> IMarker>
166 | (_: CollectionBuilder<'msg, 'marker, IMarker>, x: WidgetBuilder<'msg, Memo.Memoized<'itemMarker>>)
167 | : Content<'msg> =
168 | CollectionBuilder.yieldImpl x
169 |
170 | ///------------------
171 | type StatefulView<'arg, 'model, 'msg, 'marker when 'msg: equality> =
172 | { Init: 'arg -> 'model
173 | Update: 'msg -> 'model -> 'model
174 | View: 'model -> WidgetBuilder<'msg, 'marker> }
175 |
176 | module StatefulWidget =
177 | let mkSimpleView init update view : StatefulView<_, _, _, _> =
178 | { Init = init
179 | Update = update
180 | View = view }
181 |
182 |
183 | module Run =
184 | type Instance<'arg, 'model, 'msg, 'marker when 'msg: equality>(program: StatefulView<'arg, 'model, 'msg, 'marker>) =
185 | let mutable state: ('model * obj * Widget) option = None
186 |
187 | let logger =
188 | { Log = fun _ -> ()
189 | MinLogLevel = LogLevel.Fatal }
190 |
191 | member private x.envContext = new EnvironmentContext(logger)
192 |
193 | member private x.treeContext: ViewTreeContext =
194 | { CanReuseView = ViewHelpers.canReuseView
195 | GetViewNode = ViewNode.getViewNode
196 | Logger = logger
197 | Dispatch = fun msg -> unbox<'msg> msg |> x.ProcessMessage
198 | GetComponent = Component.getComponent
199 | SetComponent = Component.setComponent
200 | SyncAction = fun fn -> fn() }
201 |
202 | member x.ProcessMessage(msg: 'msg) =
203 | match state with
204 | | None -> ()
205 | | Some(m, target, oldWidget) ->
206 | let newModel = program.Update msg m
207 | let newWidget = program.View(newModel).Compile()
208 |
209 | // is it better to have a Kind prop instead
210 | // basically we care if it is exactly the same widget type as it was before
211 | // TODO support mount + unmount
212 | // TODO possibly introduce some notion of a platform/runtime context
213 | // that can mount and unmount nodes
214 |
215 | let viewNode = ViewNode.getViewNode target
216 |
217 | if newWidget.Key <> oldWidget.Key then
218 | failwith "type mismatch!"
219 |
220 | state <- Some(newModel, target, newWidget)
221 |
222 | Reconciler.update x.treeContext.CanReuseView (ValueSome oldWidget) newWidget viewNode
223 | ()
224 |
225 | member x.Start(arg: 'arg) =
226 | let model = program.Init(arg)
227 | let widget = program.View(model).Compile()
228 | let widgetDef = WidgetDefinitionStore.get widget.Key
229 |
230 | let struct (_node, view) =
231 | widgetDef.CreateView(widget, x.envContext, x.treeContext, ValueNone)
232 |
233 | state <- Some(model, view, widget)
234 |
235 | view :?> TestViewElement
236 |
237 | //module View =
238 | // let inline map (fn: 'oldMsg -> 'newMsg) (this: WidgetBuilder<'oldMsg, 'marker>) : WidgetBuilder<'newMsg, 'marker> =
239 | // this.MapMsg fn
240 |
--------------------------------------------------------------------------------
/src/Fabulous/Cmd.fs:
--------------------------------------------------------------------------------
1 | namespace Fabulous
2 |
3 | open System.Threading
4 | open System.Threading.Tasks
5 |
6 | /// Dispatch - feed new message into the processing loop
7 | type Dispatch<'msg> = 'msg -> unit
8 |
9 | /// Subscription - return immediately, but may schedule dispatch of a message at any time
10 | type Effect<'msg> = Dispatch<'msg> -> unit
11 |
12 | /// Cmd - container for effects that may produce messages
13 | type Cmd<'msg> = Effect<'msg> list
14 |
15 | /// Cmd module for creating and manipulating commands
16 | []
17 | module Cmd =
18 | /// Execute the commands using the supplied dispatcher
19 | let internal exec onError (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) =
20 | cmd
21 | |> List.iter(fun call ->
22 | try
23 | call dispatch
24 | with ex ->
25 | onError ex)
26 |
27 | /// None - no commands, also known as `[]`
28 | let none: Cmd<'msg> = []
29 |
30 | /// When emitting the message, map to another type
31 | let map (f: 'a -> 'msg) (cmd: Cmd<'a>) : Cmd<'msg> =
32 | cmd |> List.map(fun g -> (fun dispatch -> f >> dispatch) >> g)
33 |
34 | /// Aggregate multiple commands
35 | let batch (cmds: Cmd<'msg> list) : Cmd<'msg> = List.concat cmds
36 |
37 | /// Command to call the effect
38 | let ofEffect (effect: Effect<'msg>) : Cmd<'msg> = [ effect ]
39 |
40 | /// Command to issue a specific message
41 | let ofMsg (msg: 'msg) : Cmd<'msg> = [ fun dispatch -> dispatch msg ]
42 |
43 | /// Command to issue a specific message, only when Option.IsSome = true
44 | let ofMsgOption (msg: 'msg option) : Cmd<'msg> =
45 | [ fun dispatch ->
46 | match msg with
47 | | None -> ()
48 | | Some msg -> dispatch msg ]
49 |
50 | module OfFunc =
51 | /// Command to evaluate a simple function and map the result
52 | /// into success or error (of exception)
53 | let either (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
54 | let bind dispatch =
55 | try
56 | task arg |> (ofSuccess >> dispatch)
57 | with x ->
58 | x |> (ofError >> dispatch)
59 |
60 | [ bind ]
61 |
62 | /// Command to evaluate a simple function and map the success to a message
63 | /// discarding any possible error
64 | let perform (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
65 | let bind dispatch =
66 | try
67 | task arg |> (ofSuccess >> dispatch)
68 | with x ->
69 | ()
70 |
71 | [ bind ]
72 |
73 | /// Command to evaluate a simple function and map the error (in case of exception)
74 | let attempt (task: 'a -> unit) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
75 | let bind dispatch =
76 | try
77 | task arg
78 | with x ->
79 | x |> (ofError >> dispatch)
80 |
81 | [ bind ]
82 |
83 | module OfAsyncWith =
84 | /// Command that will evaluate an async block and map the result
85 | /// into success or error (of exception)
86 | let either (start: Async -> unit) (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
87 | let bind dispatch =
88 | async {
89 | let! r = task arg |> Async.Catch
90 |
91 | dispatch(
92 | match r with
93 | | Choice1Of2 x -> ofSuccess x
94 | | Choice2Of2 x -> ofError x
95 | )
96 | }
97 |
98 | [ bind >> start ]
99 |
100 | /// Command that will evaluate an async block and map the success
101 | let perform (start: Async -> unit) (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
102 | let bind dispatch =
103 | async {
104 | let! r = task arg |> Async.Catch
105 |
106 | match r with
107 | | Choice1Of2 x -> dispatch(ofSuccess x)
108 | | _ -> ()
109 | }
110 |
111 | [ bind >> start ]
112 |
113 | /// Command that will evaluate an async block and map the error (of exception)
114 | let attempt (start: Async -> unit) (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
115 | let bind dispatch =
116 | async {
117 | let! r = task arg |> Async.Catch
118 |
119 | match r with
120 | | Choice2Of2 x -> dispatch(ofError x)
121 | | _ -> ()
122 | }
123 |
124 | [ bind >> start ]
125 |
126 | /// Command that will evaluate an async block and map the success
127 | let performOption (start: Async -> unit) (task: 'a -> Async<_ option>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
128 | let bind dispatch =
129 | async {
130 | let! r = task arg
131 |
132 | match r with
133 | | Some x -> dispatch(ofSuccess x)
134 | | None -> ()
135 | }
136 |
137 | [ bind >> start ]
138 |
139 | module OfAsync =
140 | /// Command that will evaluate an async block and map the result
141 | /// into success or error (of exception)
142 | let inline either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
143 | OfAsyncWith.either Async.Start task arg ofSuccess ofError
144 |
145 | /// Command that will evaluate an async block and map the success
146 | let inline perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
147 | OfAsyncWith.perform Async.Start task arg ofSuccess
148 |
149 | /// Command that will evaluate an async block and map the error (of exception)
150 | let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
151 | OfAsyncWith.attempt Async.Start task arg ofError
152 |
153 | let inline msg (task: Async<'msg>) =
154 | OfAsyncWith.perform Async.Start (fun () -> task) () id
155 |
156 | let inline msgOption (task: Async<'msg option>) =
157 | OfAsyncWith.performOption Async.Start (fun () -> task) () id
158 |
159 | module OfAsyncImmediate =
160 | /// Command that will evaluate an async block and map the result
161 | /// into success or error (of exception)
162 | let inline either (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
163 | OfAsyncWith.either Async.StartImmediate task arg ofSuccess ofError
164 |
165 | /// Command that will evaluate an async block and map the success
166 | let inline perform (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
167 | OfAsyncWith.perform Async.StartImmediate task arg ofSuccess
168 |
169 | /// Command that will evaluate an async block and map the error (of exception)
170 | let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
171 | OfAsyncWith.attempt Async.StartImmediate task arg ofError
172 |
173 | module OfTask =
174 | /// Command to call a task and map the results
175 | let inline either
176 | ([] task: 'a -> Task<_>)
177 | (arg: 'a)
178 | ([] ofSuccess: _ -> 'msg)
179 | ([] ofError: _ -> 'msg)
180 | : Cmd<'msg> =
181 | [ fun dispatch ->
182 | backgroundTask {
183 | try
184 | let! r = task arg
185 | ofSuccess r |> dispatch
186 | with e ->
187 | ofError e |> dispatch
188 | }
189 | |> ignore> ]
190 |
191 | /// Command to call a task and map the success
192 | let inline perform ([] task: 'a -> Task<_>) (arg: 'a) ([] ofSuccess: _ -> 'msg) : Cmd<'msg> =
193 | [ fun dispatch ->
194 | backgroundTask {
195 | try
196 | let! r = task arg
197 | ofSuccess r |> dispatch
198 | with _ ->
199 | ()
200 | }
201 | |> ignore> ]
202 |
203 | /// Command to call a task and map the error
204 | let inline attempt ([] task: 'a -> #Task) (arg: 'a) ([] ofError: _ -> 'msg) : Cmd<'msg> =
205 | [ fun dispatch ->
206 | backgroundTask {
207 | try
208 | do! task arg
209 | with e ->
210 | ofError e |> dispatch
211 | }
212 | |> ignore