Skip to content

Commit

Permalink
Merge pull request #49 from fabulous-dev/components
Browse files Browse the repository at this point in the history
First draft for component support
  • Loading branch information
TimLariviere authored Nov 22, 2023
2 parents 3979c18 + 957ac8e commit c2cf71b
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 80 deletions.
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_

## [8.1.0-pre1] - 2023-11-22

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

## [8.0.1] - 2023-11-14

### Fixed
Expand Down Expand Up @@ -134,7 +139,8 @@ Essentially v2.8.1 and v8.0.0 are similar except for the required .NET version.
### Changed
- Fabulous.MauiControls has moved from the Fabulous repository to its own repository: [https://github.com/fabulous-dev/Fabulous.MauiControls](https://github.com/fabulous-dev/Fabulous.MauiControls)

[unreleased]: https://github.com/fabulous-dev/Fabulous.MauiControls/compare/8.0.1...HEAD
[unreleased]: https://github.com/fabulous-dev/Fabulous.MauiControls/compare/8.1.0-pre1...HEAD
[8.1.0-pre1]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.1.0-pre1
[8.0.1]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.1
[8.0.0]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.0
[2.8.1]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/2.8.1
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Fabulous" Version="2.4.0" />
<PackageVersion Include="Fabulous" Version="2.5.0-pre1" />
<PackageVersion Include="FsCheck.NUnit" Version="2.16.6" />
<PackageVersion Include="FSharp.Core" Version="8.0.100" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
Expand Down
6 changes: 6 additions & 0 deletions Fabulous.MauiControls.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Files", "_Solutio
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gallery", "samples\Gallery\Gallery.fsproj", "{A28D6852-F21C-4A43-93AF-CC71050028A9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fabulous", "..\Fabulous\src\Fabulous\Fabulous.fsproj", "{8BE1CC6B-2F37-43DD-B33D-F61DF161A68F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -81,6 +83,10 @@ Global
{A28D6852-F21C-4A43-93AF-CC71050028A9}.Release|Any CPU.Build.0 = Release|Any CPU
{A28D6852-F21C-4A43-93AF-CC71050028A9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{A28D6852-F21C-4A43-93AF-CC71050028A9}.Release|Any CPU.Deploy.0 = Release|Any CPU
{8BE1CC6B-2F37-43DD-B33D-F61DF161A68F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BE1CC6B-2F37-43DD-B33D-F61DF161A68F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BE1CC6B-2F37-43DD-B33D-F61DF161A68F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BE1CC6B-2F37-43DD-B33D-F61DF161A68F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{67FB01A1-1A3E-4A3B-83DC-7D63B56FB1A1} = {35A6823C-8312-4F92-818A-5117BB31A569}
Expand Down
252 changes: 180 additions & 72 deletions samples/HelloWorld/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,190 @@ namespace HelloWorld

open Fabulous
open Fabulous.Maui
open Microsoft.Maui
open Microsoft.Maui.Controls
open Microsoft.Maui.Graphics
open Microsoft.Maui.Accessibility
open Microsoft.Maui.Primitives

open type Fabulous.Maui.View

module Components =
type Fabulous.Maui.View with

static member inline SimpleComponent() =
Component() { Label("Hello Component").centerHorizontal() }

static member inline Counter() =
Component() {
let! count = State(0)

VStack() {
Label($"Count is {count.Current}").centerHorizontal()

Button("Increment", (fun () -> count.Set(count.Current + 1)))
Button("Decrement", (fun () -> count.Set(count.Current - 1)))
}
}

static member inline ParentChild_Child(count: int) =
Component() {
let! multiplier = State(1)
let countMultiplied = count * multiplier.Current

VStack() {
Label($"Count * {multiplier.Current} = {countMultiplied}").centerHorizontal()

Button("Increment Multiplier", (fun () -> multiplier.Set(multiplier.Current + 1)))
Button("Decrement Multiplier", (fun () -> multiplier.Set(multiplier.Current - 1)))
}
}

static member inline ParentChild_Parent() =
Component() {
let! count = State(1)

VStack() {
Label($"Count is {count.Current}").centerHorizontal()

Button("Increment Count", (fun () -> count.Set(count.Current + 1)))
Button("Decrement Count", (fun () -> count.Set(count.Current - 1)))

View.ParentChild_Child(count.Current)
}
}

static member inline Child(count: Binding<int>) =
Component() {
let! boundCount = count

VStack() {
Label($"Child.Count is {boundCount.Current}").centerHorizontal()

Button("Increment", (fun () -> boundCount.Set(boundCount.Current + 1)))
Button("Decrement", (fun () -> boundCount.Set(boundCount.Current - 1)))
}
}

static member inline BindingBetweenParentAndChild() =
Component() {
let! count = State(0)

VStack() {
Label($"Parent.Count is {count.Current}").centerHorizontal()

Button("Increment", (fun () -> count.Set(count.Current + 1)))
Button("Decrement", (fun () -> count.Set(count.Current - 1)))

View.Child(``$`` count)
}
}

static member inline SharedContextBetweenComponents() =
Component() {
let sharedContext = ComponentContext()

VStack() {
View.Counter().withContext(sharedContext)

View.Counter().withContext(sharedContext)
}
}

static member inline ModifiersOnComponent() =
Component() {
let! toggle = State(false)

VStack() {
Button("Toggle", (fun () -> toggle.Set(not toggle.Current)))

View
.SimpleComponent()
.background(SolidColorBrush(if toggle.Current then Colors.Red else Colors.Blue))
.padding(5.)
.textColor(Colors.White)
}
}

module App =
type Model = { Count: int; EnteredText: string }

type Msg =
| Clicked
| UserWroteSomething of string

type CmdMsg = SemanticAnnounce of string

let semanticAnnounce text =
Cmd.ofSub(fun _ -> SemanticScreenReader.Announce(text))

let mapCmd cmdMsg =
match cmdMsg with
| SemanticAnnounce text -> semanticAnnounce text

let init () = { Count = 0; EnteredText = "" }, []

let update msg model =
match msg with
| Clicked -> { model with Count = model.Count + 1 }, [ SemanticAnnounce $"Clicked {model.Count} times" ]

| UserWroteSomething text -> { model with EnteredText = text }, []

let view model =
Application(
ContentPage(
ScrollView(
(VStack(spacing = 25.) {
Image("dotnet_bot.png")
.semantics(description = "Cute dotnet bot waving hi to you!")
.height(200.)
.centerHorizontal()

Label("Hello, World!")
.semantics(SemanticHeadingLevel.Level1)
.font(size = 32.)
.centerTextHorizontal()

Label("Welcome to .NET Multi-platform App UI powered by Fabulous")
.semantics(SemanticHeadingLevel.Level2, "Welcome to dot net Multi platform App U I powered by Fabulous")
.font(size = 18.)
.centerTextHorizontal()

let text =
if model.Count = 0 then
"Click me"
else
$"Clicked {model.Count} times"

Button(text, Clicked)
.semantics(hint = "Counts the number of times you click")
.centerHorizontal()

Entry(model.EnteredText, UserWroteSomething)
.semantics(hint = "Type something here")

let userText =
if model.EnteredText = "" then
"You wrote nothing"
else
$"You wrote '{model.EnteredText}'"

Label(userText).semantics(description = userText).centerHorizontal()
})
.padding(Thickness(30., 0., 30., 0.))
.centerVertical()
open Components

open type Fabulous.Maui.View

let app () =
Component() {
let! appState = State(0)

Application(
ContentPage(
ScrollView(
(VStack(spacing = 40.) {
// App state display
VStack(spacing = 20.) {
Label("App state").centerHorizontal().font(attributes = FontAttributes.Bold)

VStack(spacing = 0.) {
Label($"AppState = {appState.Current}").centerHorizontal()

Button("Increment", (fun () -> appState.Set(appState.Current + 1)))
Button("Decrement", (fun () -> appState.Set(appState.Current - 1)))
}
}

// Simple component
VStack(spacing = 20.) {
Label("Simple component")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

SimpleComponent()
}

// Simple component with state
VStack(spacing = 20.) {
Label("Simple components with individual states")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

Counter()
Counter()
}

// Parent child component
VStack(spacing = 20.) {
Label("Parent child component")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

ParentChild_Parent()
}

// Binding between parent and child
VStack(spacing = 20.) {
Label("Binding between parent and child")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

BindingBetweenParentAndChild()
}

// Shared context between components
VStack(spacing = 20.) {
Label("Shared context between components")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

SharedContextBetweenComponents()
}

// Modifiers on component
VStack(spacing = 20.) {
Label("Modifiers on component")
.centerHorizontal()
.font(attributes = FontAttributes.Bold)

ModifiersOnComponent()
}
})
.centerVertical()
)
)
)
)

let program = Program.statefulWithCmdMsg init update view mapCmd
}
3 changes: 1 addition & 2 deletions samples/HelloWorld/HelloWorld.fsproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks>net8.0-ios;net8.0-android;net8.0-maccatalyst</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
Expand Down
2 changes: 1 addition & 1 deletion samples/HelloWorld/MauiProgram.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type MauiProgram =
static member CreateMauiApp() =
MauiApp
.CreateBuilder()
.UseFabulousApp(App.program)
.UseFabulousApp(App.app)
.ConfigureFonts(fun fonts ->
fonts
.AddFont("OpenSans-Regular.ttf", "OpenSansRegular")
Expand Down
27 changes: 25 additions & 2 deletions src/Fabulous.MauiControls/AppHostBuilderExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

open Fabulous
open System.Runtime.CompilerServices
open Microsoft.Maui.Controls
open Microsoft.Maui.Hosting
open Microsoft.Maui.Controls.Hosting
open System
Expand All @@ -10,8 +11,30 @@ open System
type AppHostBuilderExtensions =
[<Extension>]
static member UseFabulousApp(this: MauiAppBuilder, program: Program<unit, 'model, 'msg, #IFabApplication>) : MauiAppBuilder =
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplication program) :> Microsoft.Maui.IApplication)
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) ->
Component.registerComponentFunctions()
(Program.startApplication program) :> Microsoft.Maui.IApplication)

[<Extension>]
static member UseFabulousApp(this: MauiAppBuilder, program: Program<'arg, 'model, 'msg, #IFabApplication>, arg: 'arg) : MauiAppBuilder =
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationWithArgs arg program) :> Microsoft.Maui.IApplication)
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) ->
Component.registerComponentFunctions()
(Program.startApplicationWithArgs arg program) :> Microsoft.Maui.IApplication)

[<Extension>]
static member inline UseFabulousApp(this: MauiAppBuilder, [<InlineIfLambda>] root: unit -> WidgetBuilder<'msg, #IFabApplication>) : MauiAppBuilder =
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) ->
Component.registerComponentFunctions()

let widget = root().Compile()
let widgetDef = WidgetDefinitionStore.get widget.Key

let viewTreeContext =
{ CanReuseView = MauiViewHelpers.canReuseView
GetViewNode = ViewNode.get
Logger = MauiViewHelpers.defaultLogger()
Dispatch = ignore }

let struct (_node, view) = widgetDef.CreateView(widget, viewTreeContext, ValueNone)

view :?> Microsoft.Maui.IApplication)
20 changes: 20 additions & 0 deletions src/Fabulous.MauiControls/Component.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Fabulous.Maui

open Fabulous
open Microsoft.Maui.Controls

module Component =
let ComponentProperty =
BindableProperty.CreateAttached("Component", typeof<Component>, typeof<BindableObject>, null)

let registerComponentFunctions () =
Component.SetComponentFunctions(
(fun view -> (view :?> BindableObject).GetValue(ComponentProperty) :?> Component),
(fun view comp -> (view :?> BindableObject).SetValue(ComponentProperty, comp))
)

[<AutoOpen>]
module ComponentBuilders =
type Fabulous.Maui.View with

static member inline Component<'msg, 'marker>() = ComponentBuilder()
Loading

0 comments on commit c2cf71b

Please sign in to comment.