Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add new Component API #1051

Merged
merged 7 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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