Skip to content

Commit

Permalink
Merge pull request #1051 from fabulous-dev/components
Browse files Browse the repository at this point in the history
Add new Component API
  • Loading branch information
TimLariviere committed Nov 22, 2023
2 parents a5d4832 + 23d371e commit 06c3890
Show file tree
Hide file tree
Showing 8 changed files with 652 additions and 1 deletion.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

_No unreleased changes_

## [2.5.0-preview.1] - 2023-11-22

### Added
- Add new Component API by @TimLariviere (https://github.com/fabulous-dev/Fabulous/pull/1051)

## [2.4.0] - 2023-08-07

### Changed
Expand Down Expand Up @@ -50,7 +55,8 @@ _No unreleased changes_
### Changed
- Fabulous.XamarinForms & Fabulous.MauiControls have been moved been out of the Fabulous repository. Find them in their own repositories: [https://github.com/fabulous-dev/Fabulous.XamarinForms](https://github.com/fabulous-dev/Fabulous.XamarinForms) / [https://github.com/fabulous-dev/Fabulous.MauiControls](https://github.com/fabulous-dev/Fabulous.MauiControls)

[unreleased]: https://github.com/fabulous-dev/Fabulous/compare/2.4.0...HEAD
[unreleased]: https://github.com/fabulous-dev/Fabulous/compare/2.5.0-preview.1...HEAD
[2.5.0-preview.1]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.5.0-preview.1
[2.4.0]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.4.0
[2.3.2]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.3.2
[2.3.1]: https://github.com/fabulous-dev/Fabulous/releases/tag/2.3.1
Expand Down
28 changes: 28 additions & 0 deletions src/Fabulous/Attributes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,31 @@ module Attributes =
|> AttributeDefinitionStore.registerScalar

{ Key = key; Name = name }

let inline defineEventNoArgNoDispatch
name
([<InlineIfLambda>] getEvent: obj -> IEvent<EventHandler, EventArgs>)
: SimpleScalarAttributeDefinition<unit -> unit> =
let key =
SimpleScalarAttributeDefinition.CreateAttributeData(
ScalarAttributeComparers.noCompare,
(fun _ (newValueOpt: (unit -> unit) voption) node ->
let event = getEvent(node.Target)

match node.TryGetHandler(name) with
| ValueNone -> ()
| ValueSome handler -> event.RemoveHandler handler

match newValueOpt with
| ValueNone -> node.SetHandler(name, ValueNone)

| ValueSome(fn) ->
let handler = EventHandler(fun _ _ -> fn())

event.AddHandler handler
node.SetHandler(name, ValueSome handler))
)

|> AttributeDefinitionStore.registerScalar

{ Key = key; Name = name }
71 changes: 71 additions & 0 deletions src/Fabulous/Component/Binding.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Fabulous

open System.Runtime.CompilerServices

(*
The idea of Binding is to listen to a State<'T> that is managed by another Context and be able to update it
while notifying the two Contexts involved (source and target)
let child (count: BindingRequest<int>) =
view {
let! boundCount = bind count
Button($"Count is {boundCount.Value}", fun () -> boundCount.Set(boundCount.Value + 1))
}
let parent =
view {
let! count = state 0
VStack() {
Text($"Count is {count.Value}")
child (Binding.ofState count)
}
}
*)

type Binding<'T> = delegate of unit -> StateValue<'T>

[<Struct>]
type BindingValue<'T> =
val public Context: ComponentContext
val public SourceContext: ComponentContext
val public SourceKey: int
val public SourceCurrentValue: 'T

new(ctx, sourceCtx, sourceKey, sourceCurrentValue) =
{ Context = ctx
SourceContext = sourceCtx
SourceKey = sourceKey
SourceCurrentValue = sourceCurrentValue }

member inline this.Current = this.SourceCurrentValue

member inline this.Set(value: 'T) =
this.SourceContext.SetValue(this.SourceKey, value)
this.Context.NeedsRender()

[<Extension>]
type BindingExtensions =
[<Extension>]
static member inline Bind
(
_: ComponentBuilder,
[<InlineIfLambda>] request: Binding<'T>,
[<InlineIfLambda>] continuation: BindingValue<'T> -> ComponentBodyBuilder<'msg, 'marker>
) =
// Despite its name, ComponentBinding actual value is not stored in this component, but in the source component
// So, we do not need to increment the number of bindings here
ComponentBodyBuilder(fun bindings ctx ->
let source = request.Invoke()

source.Context.RenderNeeded.Add(fun () -> ctx.NeedsRender())

let state = BindingValue<'T>(ctx, source.Context, source.Key, source.Current)
(continuation state).Invoke(bindings, ctx))

[<AutoOpen>]
module BindingHelpers =
let inline ``$`` (source: StateValue<'T>) = Binding(fun () -> source)
33 changes: 33 additions & 0 deletions src/Fabulous/Component/Builder.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Fabulous

/// Delegate used by the ComponentBuilder to compose a component body
/// It will be aggressively inlined by the compiler leaving no overhead, only a pure function that returns a WidgetBuilder
type ComponentBodyBuilder<'msg, 'marker> =
delegate of bindings: int<binding> * context: ComponentContext -> struct (int<binding> * WidgetBuilder<'msg, 'marker>)

type ComponentBuilder() =
member inline this.Yield(widgetBuilder: WidgetBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx -> struct (bindings, widgetBuilder))

member inline this.Combine([<InlineIfLambda>] a: ComponentBodyBuilder<'msg, 'marker>, [<InlineIfLambda>] b: ComponentBodyBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx ->
let struct (bindingsA, _) = a.Invoke(bindings, ctx) // discard the previous widget in the chain but we still need to count the bindings
let struct (bindingsB, resultB) = b.Invoke(bindings, ctx)

// Calculate the total number of bindings between A and B
let resultBindings = (bindingsA + bindingsB) - bindings

struct (resultBindings, resultB))

member inline this.Delay([<InlineIfLambda>] fn: unit -> ComponentBodyBuilder<'msg, 'marker>) =
ComponentBodyBuilder<'msg, 'marker>(fun bindings ctx ->
let sub = fn()
sub.Invoke(bindings, ctx))

member inline this.Run([<InlineIfLambda>] body: ComponentBodyBuilder<'msg, 'marker>) =
let compiledBody =
ComponentBody(fun ctx ->
let struct (_, result) = body.Invoke(0<binding>, ctx)
struct (ctx, result.Compile()))

WidgetBuilder<'msg, 'marker>(Component.WidgetKey, Component.Body.WithValue(compiledBody))
Loading

0 comments on commit 06c3890

Please sign in to comment.