timlariviere / fabulous-new Goto Github PK
View Code? Open in Web Editor NEWFabulous v2 - Work in progress
Home Page: https://timothelariviere.com/Fabulous-new/
License: Other
Fabulous v2 - Work in progress
Home Page: https://timothelariviere.com/Fabulous-new/
License: Other
TDB
In v1, we created Fabulous.CodeGen
to automate the creation of wrappers around Xamarin.Forms controls.
It works by taking a mapping file (Xamarin.Forms.Core.json) which has the path to dlls and how we should map the types inside those dlls.
It outputs 2 F# files with the attribute keys and wrappers for all mapped controls.
The idea is to reuse Fabulous.CodeGen
and adapt it so it can generate both Xamarin.Forms.Core.Attributes.fs and Xamarin.Forms.Core.fs
Doing this will avoid us to implement everything by hand and make a lot more controls available in v2.
Would be good if we can add support for XML documentation Add Extension https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/xml-documentation#recommended-tags. This will make really easy to use
We need a way to cut off diffing. In V1 there was a clever trick that involved getting the location of a function type.
It does seem like a hack to me, but very clever! ^_^.
Unfortunately, we cannot directly port it to v2 due to new reconciler. For example it works with structs vs reference types.
So, we need a similar way to cut off diffing of a branch. Note that in that case diffing of a branch is going to be way slower than a few allocations, thus it is totally ok to use dynamic dispatch + heap allocated values there.
I would propose to store some data in ViewNode (seems like a good place for that) vs global weak map used in V1. That is how React and Elm do that (at least conceptually).
2 ways to represent item collection controls such as ListView, CollectionView, CarouselView, etc.
// Virtualized list
ListView(items, fun item -> item.Id, fun item ->
TextCell(item.Text)
)
// Explicit list
ListView([
for item in items do
TextCell(item.Text)
.key(item.Id)
])
In v1, we're only supporting explicit lists but it has a lot of constraints.
Virtualization is hard to do and if you happen to have 1 million items in your list, it would completely kill the performance (view function + diffing).
So I think it would be better to create 2 kinds of lists ListView
+ VirtualizedListView
to support better virtualization.
One thing I was thinking about if we can make convert functions optional (ValueOption
). My understanding that it is id
function mist of the time. If that is true, skipping calling it with checking for ValueSome
is probably worth it in terms of perf.
BUT! it is probably going to be tricky to type properly, given that then we will have to convert 'modelType -> 'valueType
.
So maybe a union can be a solution there.
just a sketch:
type ScalarAttributeDefinition<'inputType, 'modelType, 'valueType> =
| NoConverters of .... // 'inputType = 'modelType = 'valueType, are all the same
| JustModelConverted of ... // 'inputType <> 'modelType , 'valueType ='modelType
| JustValueConverter of ... // 'inputType = 'modelType , 'valueType <> 'modelType
| ModelAndValueConverters of ... // 'inputType <> 'modelType <> 'valueType, are all different
Feels very clunky but possible worth considering
Originally posted by @twop in #33 (comment)
There are still somethings are not wrapped . Like Effects, Behaviours, VisualStateManagers, TemplatedView , TemplatedPages, StyleWidgets. Would be good to create a separate issue for all them so we can attempt then in the future.
Originally posted by @edgarfgp in #53 (comment)
public static Constraint RelativeToView(View view, Func<RelativeLayout, View, double> measure)
{
var result = new Constraint { _measureFunc = layout => measure(layout, view), RelativeTo = new[] { view } };
return result;
}
public static BoundsConstraint FromExpression(Expression<Func<Rectangle>> expression, IEnumerable<View> parents = null)
{
return FromExpression(expression, false, parents);
}
internal static BoundsConstraint FromExpression(Expression<Func<Rectangle>> expression, bool fromExpression, IEnumerable<View> parents = null)
{
Func<Rectangle> compiled = expression.Compile();
var result = new BoundsConstraint
{
_measureFunc = compiled,
RelativeTo = parents ?? ExpressionSearch.Default.FindObjects<View>(expression).ToArray(), // make sure we have our own copy
CreatedFromExpression = fromExpression
};
return result;
}
It would be great to enable styling through specialized Widgets like LabelStyle()
, ButtonStyle()
, etc.
Those styles would be passed to .styles([...])
modifiers at both the control level and application level.
Label("Hello")
.style(
// Local style
LabelStyle()
.textColor(Color.Blue)
.font(FontFamily.OpenSans)
)
)
Application(
ContentPage(
VerticalStackLayout([
Button("Click me", OnClicked)
Button("Destroy the world", OnDestroyClicked)
.styleKey("destructive")
])
)
)
.styles([
// Global style applied to all buttons
ButtonStyle()
.backgroundColor(light = Color.Gray, dark = Color.Yellow)
// Global style only applied to buttons with style key `destructive`
ButtonStyle("destructive")
.textColor(Color.Red)
])
The idea of those widgets will be to make them map to
<Application>
<Application.Resources>
<Style TargetType="Button">
<Setter PropertyName="BackgroundColor" Value="{AppThemeBinding Light=Color.Gray, Dark=Color.Yellow}" />
</Style>
<Style TargetType="Button" Key="destructive">
<Setter PropertyName="TextColor" Value="Color.Red" />
</Style>
</Application.Resources>
</Application>
To implement this, you can take a look at Xamarin.Forms.Core.Attributes.fs and Xamarin.Forms.Core.fs.
I think the setters will be declared as attributes (via a custom defineWidget
=> defineSetter
) and styles like widgets (type [<Struct>] LabelStyle (...)
).
If you want to work on this task, put a message after this comment.
Also try putting everything you write in a new file (eg. Xamarin.Forms.Core.Style.fs
) so you won't have too many conflicts if we change stuff.
As long as I was managed to run Elmish.Uno on WinUI
<TargetFrameworks>net6.0;net6.0-windows10.0.22000.0</TargetFrameworks>
And now we can call WinRT API using F#.
I think that it is a good idea to propose to build Fabulous v2 on top of WinUI targeting WinUI Uno Platform as it has better performance and is better designed than Xamarin.Forms/MAUI
In v1, we were exposing dispatch
in the view
function and it was up to the developers to map messages to the right root type.
type Msg =
| SubPageMsg of SubPage.Msg
let view model dispatch =
SubPage.view model.SubModel (SubPageMsg >> dispatch)
This was possible because we weren't strongly typing the views (everything is ViewElement
).
dispatch
is a function internal to Fabulous and it's better to not leak it for the view
function where it can be misused.
To avoid this, in v2 we're strongly typing the widgets (eg. ContentPage<SubPage.Msg>
) and Fabulous takes care of dispatching internally without exposing it to the developers.
But this strong typing prevents composing widgets of different msg types.
NavigationPage([
MainPage.view model.MainPageModel
DetailPage.view model.DetailPageModel // Error: Expected to be ContentPage<MainPage.Msg>, but was ContentPage<DetailPage.Msg>
])
So to change the msg type from one to another (ContentPage<MainPage.Msg>
=> ContentPage<DetailPage.Msg>
), having a mapping function would be necessary
type Msg =
| MainMsg of MainPage.Msg
| DetailMsg of DetailPage.Msg
NavigationPage([
Widget.map MainMsg (MainPage.view model.MainPageModel)
Widget.map DetailMsg (DetailPage.view model.DetailPageModel)
])
Hello, I noticed Fantomas was added in #40.
Many thanks for doing this.
I did also see that you are checking whether the code is formatted using dotnet fantomas --check **/*.fs
.
Wildcards are not supported there, you best pass along the folder you want to check, included by the recurse flag.
Something like dotnet fantomas --check -r src
.
Would be great to have a control gallery apps to both test out the wrappers and have a sample on how to use them.
Pages:
Layouts:
Controls:
Collections:
Cells:
MenuItems:
GestureRecognizers:
I think we should add a debounce function like in V1 https://github.com/fsprojects/Fabulous/blob/8bbb610a2bbfe239e175b8e25ac950d55ebc94f5/Fabulous.XamarinForms/src/Fabulous.XamarinForms/ViewHelpers.fs#L66
We need to write all the decisions we made for the architecture of v2, including details on:
We can enable all the animation methods in : https://docs.microsoft.com/en-us/dotnet/api/xamarin.forms.viewextensions?view=xamarin-forms
Currently we benchmark with .NET 6 JIT, while it is awesome to have something going, we probably want a closer to Fabulous actual use case: mobile AoT compilation with Mono.
We use https://benchmarkdotnet.org that can support it, I just haven't had enough energy to figure out how. Note that I have Apple M1 chip which can run arm code natively (thus the closest that we can get)
Here is a snippet how to setup Mono https://benchmarkdotnet.org/articles/configs/toolchains.html#monoaotllvm
Alternatively we can have a test mobile project (not even XF) with a simple functionality of Run
and Report/See results
that we manually deploy to a device.
Note for the benchmark app above we would still use TestUI framework vs Xamarin binding to have a stable reference and "apples to apples" comparison
When toggling the button, diffing by mistake change the label of the Toggle
button
namespace CounterApp
open Fabulous
open Fabulous.XamarinForms
open type Fabulous.XamarinForms.View
module App =
type Model =
{ Flag: bool }
type Msg =
| Toggle
let init () =
{ Flag = false }
let update msg model =
match msg with
| Toggle ->
{ model with Flag = not model.Flag }
let view model =
Application(
ContentPage(
"CounterApp",
(VStack() {
Button("Toggle", Toggle)
if model.Flag then
Label("Hello")
Button("World", Toggle)
})
.padding(30.)
.centerVertical ()
)
)
let program =
Program.statefulApplication init update view
Currently adding an attribute to a widget involves a heap allocation of a new array.
Copy pasting (at the moment of creation of this issue):
static member inline AddScalarAttribute(this: ^T :> IWidgetBuilder, attr: ScalarAttribute) =
let attribs = this.ScalarAttributes
let attribs2 = Array.zeroCreate(attribs.Length + 1) // <--- allocating a new one
Array.blit attribs 0 attribs2 0 attribs.Length
attribs2.[attribs.Length] <- attr
let result =
(^T: (new : ScalarAttribute [] * WidgetAttribute [] * WidgetCollectionAttribute [] -> ^T) (attribs2,
this.WidgetAttributes,
this.WidgetCollectionAttributes))
result
let attribs = this.ScalarAttributes
) are short lived and apply pressure on GCUse a technique commonly used in high performance computing when you know that arrays are going to be mostly small.
pseudocode:
[<Struct>]
type StackArrayN<'a> = {
Size: uint
Item0: 'a // initialized with 'default'
Item1: 'a
....
ItemN: 'a
PostN: 'a[] // needs to be initialized with null. e.g. 'default'
}
As you might guessed it is a bit tricky to work with and it needs to be explicitly written for each N. BUT it doesn't allocate anything before we reach N. Which makes traversal and creation very CPU cache friendly (in a happy path)
Luckily we have only a handful of use cases when it is applicable
There is a bug when we remove some attributes and add new ones from a control that is reused by Fabulous.
It is specific when the removed attributes have a higher "key" than the new "ones", and there is more new ones than old ones.
It's quite difficult to explain, but here is a repro:
let Attr1 = Attributes.define ... // Key = 0
let Attr2 = Attributes.define ... // Key = 1
let Attr3 = Attributes.define ... // Key = 2
let Attr4 = Attributes.define ... // Key = 3
let Attr5 = Attributes.define ... // Key = 4
if not model.Flag then
Button()
.attr4()
.attr5()
else
Button()
.attr1()
.attr2()
.attr3()
// Start: model.Flag = false
// When switching model.Flag to true, an index out of range exception is raised inside ScalarAttributesChangesEnumerator
This will allow us to reduce the use of ViewRef or the need to match against the platform :
Just curious, I have a project that would benefit from using Fabulous V2 and I'm not worried about bugs, corner cases, etc, just want something basic I can architect around and use with other devs, not in production.
Are y'all at a useful point here for that kind of early integration, with my expectation that there will be bugs and API instability? Or should I just wait?
In V1 we have some testing helper functions ie findViewElement and the ElementViewers to make the view and Widgets testable . https://fsprojects.github.io/Fabulous/Fabulous.XamarinForms/dev-testing.html
Are we doing the same for V2 ?
Currently we calculate diff recursively for the entire tree all at once. As a part of it we use a recursive data structure
and [<Struct; RequireQualifiedAccess>] WidgetChange =
| Added of added: WidgetAttribute
| Removed of removed: WidgetAttribute
| Updated of updated: struct (WidgetAttribute * WidgetDiff)
| ReplacedBy of replacedBy: WidgetAttribute
and [<Struct; RequireQualifiedAccess>] WidgetCollectionChange =
| Added of added: WidgetCollectionAttribute
| Removed of removed: WidgetCollectionAttribute
| Updated of updated: struct (WidgetCollectionAttribute * WidgetCollectionItemChange [])
and [<Struct; RequireQualifiedAccess>] WidgetCollectionItemChange =
| Insert of widgetInserted: struct (int * Widget)
| Replace of widgetReplaced: struct (int * Widget)
| Update of widgetUpdated: struct (int * WidgetDiff)
| Remove of removed: int
and [<Struct>] WidgetDiff =
{
ScalarChanges: ScalarChange []
WidgetChanges: WidgetChange [] // <--- children's update
WidgetCollectionChanges: WidgetCollectionChange [] // <--- children's update
}
There a a couple of concerns here:
ApplyDiff
they immediately become garbage applying pressure on GC.Make calculation of children diff somehow lazy. Note that it potentially ok to have dynamic dispatch (virtual call) if it removes allocations. I haven't researched enough the new feature of "Resumable Code" in F# 6, to understand if that idea is even applicable to us.
But roughly it would be awesome if we can somehow express this (pseudo code):
and [<Struct>] WidgetDiff =
{
ScalarChanges: ScalarChange []
WidgetChanges: LazyCollection<WidgetChange> // <---
WidgetCollectionChanges: LazyCollection<WidgetCollectionChange> // <---
}
My intuition tells me that in this form LazyCollection
has to be dynamically allocated, thus it is probably not better than status quo.
HOWEVER this is probably an opportunity to express the same recursive Diff via some other data structure. Like so
type WidgetDiff = Stream<struct (ScalarChange [] * WidgetChange[] * WidgetCollectionChange[])>
Where the Stream
data structure might be "Resumable Code". If so then we can replace []
with StackArray
(stack allocated collection), thus avoiding intermediate allocations. Not sure if it is possible to avoid dynamic dispatch though.
Research what are the options making diff calculation lazy, and have a PoC ideally backed by benchmark how it might work.
Note that it is probably going to be one of the last perf explorations (at least according to my intuition), because other "[Performance]" tasks probably have more predictable ROI.
As it stands we have boxing/unboxing shenanigans accessing Attribute values.
simplified pseudocode:
let paddingKey = 4424 // opaque int number
let paddingValue = 3.14 // float value to store in attribute
let padding = Attribute(paddingKey, box paddingValue) // note 'box' here
unbox paddingAttr.Value
at least twice. And probably one more time to actually apply the diff. Which is costly due to cache misses and runtime checks to call unbox
bool
can be encoded as int
using simply 0 | 1
Attribute
of float
and uint64
, actual backing type are TBDsimplified pseudocode:
// converters from user facing value to backing value
// in this example it is 'id' function
let convertFrom = fun p -> p
let convertTo = fun p -> p
let paddingAttr = defineFloatStoredAttr("padding", convertFrom, convertTo)
// will set a flag in the key that it is float backed attribute
let padding = paddingAttr.WithValue(3.14) // no boxing nor allocations
// roughly translates to
// {Key = paddingKey ||| FLOAT_ATTR_TYPE; Value = null; FloatVal = 3.14; UintVal = 0}
Now using that knowledge we can optimize diffing like so
if attr.Key &&& FLOAT_ATTR_TYPE then
// optimized case
a.FloatVal = b.FloatVal
else
// general case, what we have currently
let comparer = getComparer(attr.Key) // <-- cache miss
comparer.Compare(a.Value, b.Value) // dynamic dispatch + unboxing
(8 bytes + 8 bytes) * 1000 = 16Kb
. Probably totally fine for any reasonably scenario.[<Struct>]
type Attribute =
| FloatAttr of key: int * floatVal: float
| BoxedAttr of key: int * boxedVal: obj
// will produce Error: "key" property is duplicated :(
// also not safe, e.g. for the same key value we can have different variants
Hi,
I’ve been casually following the development of Fabulous V2. Awesome work, really impressive stuff. I was just taking a look at the changes in diffing, and I wanted to put something on your radar, and was wondering if it had been considered. From a brief skim of the code, it kind of looks like each control is wrapped in a kind of builder (WidgetBuidler, CollectionBuilder, etc) that is responsible for being able to calculate the different types of things that could’ve changed (ScalarAttribute, WidgetAttribute, etc). And then the diffing basically takes two Widgets, and then diffs the attributes.
One thing I was thinking about is whether something like https://fsprojects.github.io/FSharp.Data.Adaptive/ could be useful for this. The rough outline of how I was thinking it would work is that each control could have an “adaptive” version generated via https://github.com/krauthaufen/Adaptify/, and then the Fabulous DSL would be responsible for updating values via the FSharp.Data.Adaptive API (transact
function). The user-facing API/DSL could stay the same as it is now. And then when the program would want to diff, it would be able to get a near-perfect incremental re-computation of whatever you would want to recompute. Unfortunately, this would be a somewhat intrusive change at the moment, since this would change how the actual data about diffs would be stored (rather than simply changing the diffing algorithm). But I figured I would mention it now while there's still a lot of things in flux.
Anyway, food for thought.
The goal is to have something that we can rely on while working on perf optimizations. Thus, this has to be done first before other changes
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.