Skip to content

Commit

Permalink
Merge pull request #55 from fabulous-dev/perf-optimizations
Browse files Browse the repository at this point in the history
Additional performance optimizations
  • Loading branch information
TimLariviere authored Jan 10, 2024
2 parents e40d296 + d692191 commit 67e487a
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 93 deletions.
10 changes: 8 additions & 2 deletions 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.0.5] - 2024-01-10

### Changed
- Additional performance optimizations by @TimLariviere (https://github.com/fabulous-dev/Fabulous.MauiControls/pull/55)

## [8.0.4] - 2024-01-08

### Changed
Expand Down Expand Up @@ -150,8 +155,9 @@ 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.3...HEAD
[8.0.3]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.3
[unreleased]: https://github.com/fabulous-dev/Fabulous.MauiControls/compare/8.0.5...HEAD
[8.0.5]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.5
[8.0.4]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.4
[8.0.2]: https://github.com/fabulous-dev/Fabulous.MauiControls/releases/tag/8.0.2
[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
Expand Down
20 changes: 16 additions & 4 deletions src/Fabulous.MauiControls/AppHostBuilderExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,28 @@ 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) ->
let app = Program.startApplication program
Theme.ListenForChanges(app)
app)

[<Extension>]
static member UseFabulousApp(this: MauiAppBuilder, program: Program<unit, 'model, 'msg, Memo.Memoized<#IFabApplication>>) : MauiAppBuilder =
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationMemo program) :> Microsoft.Maui.IApplication)
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) ->
let app = Program.startApplicationMemo program
Theme.ListenForChanges(app)
app)

[<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) ->
let app = Program.startApplicationWithArgs arg program
Theme.ListenForChanges(app)
app)

[<Extension>]
static member UseFabulousApp(this: MauiAppBuilder, program: Program<'arg, 'model, 'msg, Memo.Memoized<#IFabApplication>>, arg: 'arg) : MauiAppBuilder =
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) -> (Program.startApplicationWithArgsMemo arg program) :> Microsoft.Maui.IApplication)
this.UseMauiApp(fun (_serviceProvider: IServiceProvider) ->
let app = Program.startApplicationWithArgsMemo arg program
Theme.ListenForChanges(app)
app)
31 changes: 31 additions & 0 deletions src/Fabulous.MauiControls/Attributes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ open Fabulous
open Fabulous.ScalarAttributeDefinitions
open Microsoft.Maui.Controls
open System
open System.IO
open Microsoft.Maui

[<RequireQualifiedAccess>]
type ImageSourceValue =
| Source of source: IImageSource
| File of file: string
| Uri of uri: Uri
| Stream of stream: Stream

[<Struct>]
type ValueEventData<'data, 'eventArgs> =
Expand Down Expand Up @@ -106,6 +115,28 @@ module Attributes =
| ValueNone -> target.ClearValue(bindableProperty)
| ValueSome v -> target.SetValue(bindableProperty, v))

/// Performance optimization: avoid allocating a new ImageSource instance on each update
/// we store the user value (eg. string, Uri, Stream) and convert it to an ImageSource only when needed
let inline defineBindableImageSource (bindableProperty: BindableProperty) =
Attributes.defineScalar<ImageSourceValue, ImageSourceValue>
bindableProperty.PropertyName
id
ScalarAttributeComparers.equalityCompare
(fun _ newValueOpt node ->
let target = node.Target :?> BindableObject

match newValueOpt with
| ValueNone -> target.ClearValue(bindableProperty)
| ValueSome v ->
let value =
match v with
| ImageSourceValue.Source source -> source
| ImageSourceValue.File file -> ImageSource.FromFile file
| ImageSourceValue.Uri uri -> ImageSource.FromUri uri
| ImageSourceValue.Stream stream -> ImageSource.FromStream(fun () -> stream)

target.SetValue(bindableProperty, value))

/// Define an attribute storing a Widget for a bindable property
let inline defineBindableWidget (bindableProperty: BindableProperty) =
Attributes.definePropertyWidget
Expand Down
11 changes: 8 additions & 3 deletions src/Fabulous.MauiControls/Fabulous.MauiControls.fsproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<UseLocalProjectReference>false</UseLocalProjectReference>

<TargetFrameworks>net8.0</TargetFrameworks>
<UseMaui>true</UseMaui>
<IsPackable>true</IsPackable>
</PropertyGroup>
Expand Down Expand Up @@ -152,9 +154,12 @@
This version will be used as the lower bound in the NuGet package
-->
<PackageReference Include="FSharp.Core" VersionOverride="8.0.100" PrivateAssets="All" />
<PackageReference Include="Fabulous" VersionOverride="[2.4.0, 2.5.0)" />
<PackageReference Include="Fabulous" Condition="'$(UseLocalProjectReference)' != 'true'" VersionOverride="[2.4.0, 2.5.0)" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="Microsoft.Maui.Controls" VersionOverride="8.0.3" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" VersionOverride="8.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Condition="'$(UseLocalProjectReference)' == 'true'" Include="..\..\..\Fabulous\src\Fabulous\Fabulous.fsproj" />
</ItemGroup>
</Project>
18 changes: 11 additions & 7 deletions src/Fabulous.MauiControls/ThemeAware.fs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
namespace Fabulous.Maui

open Microsoft.Maui.Controls
open Microsoft.Maui.ApplicationModel
open Fabulous
open Fabulous.Maui

[<AbstractClass; Sealed>]
type Theme =
static let mutable _currentTheme = AppTheme.Unspecified
static member Current = _currentTheme

static member ListenForChanges(app: Application) =
app.RequestedThemeChanged.Add(fun args -> _currentTheme <- args.RequestedTheme)

[<AbstractClass; Sealed>]
type ThemeAware =
static member With(light: 'T, dark: 'T) =
if AppInfo.RequestedTheme = AppTheme.Dark then
dark
else
light
if Theme.Current = AppTheme.Dark then dark else light

module ThemeAwareProgram =
type Model<'model> = { Theme: AppTheme; Model: 'model }
Expand All @@ -22,9 +28,7 @@ module ThemeAwareProgram =
let init (init: 'arg -> 'model * Cmd<'msg>) (arg: 'arg) =
let model, cmd = init arg

{ Theme = AppInfo.RequestedTheme
Model = model },
Cmd.map ModelMsg cmd
{ Theme = Theme.Current; Model = model }, Cmd.map ModelMsg cmd

let update (update: 'msg * 'model -> 'model * Cmd<'msg>) (msg: Msg<'msg>, model: Model<'model>) =
match msg with
Expand Down
23 changes: 17 additions & 6 deletions src/Fabulous.MauiControls/Views/Cells/ImageCell.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ type IFabImageCell =
module ImageCell =
let WidgetKey = Widgets.register<ImageCell>()

let ImageSource =
Attributes.defineBindableWithEquality ImageCell.ImageSourceProperty
let ImageSource = Attributes.defineBindableImageSource ImageCell.ImageSourceProperty

[<AutoOpen>]
module ImageCellBuilders =
Expand All @@ -23,25 +22,37 @@ module ImageCellBuilders =
/// <param name="text">The text of the cell</param>
/// <param name="source">The image source</param>
static member inline ImageCell<'msg>(text: string, source: ImageSource) =
WidgetBuilder<'msg, IFabImageCell>(ImageCell.WidgetKey, TextCell.Text.WithValue(text), ImageCell.ImageSource.WithValue(source))
WidgetBuilder<'msg, IFabImageCell>(
ImageCell.WidgetKey,
TextCell.Text.WithValue(text),
ImageCell.ImageSource.WithValue(ImageSourceValue.Source source)
)

/// <summary>Create an ImageCell widget with a text and an image source</summary>
/// <param name="text">The text of the cell</param>
/// <param name="source">The image source</param>
static member inline ImageCell<'msg>(text: string, source: string) =
View.ImageCell<'msg>(text, ImageSource.FromFile(source))
WidgetBuilder<'msg, IFabImageCell>(
ImageCell.WidgetKey,
TextCell.Text.WithValue(text),
ImageCell.ImageSource.WithValue(ImageSourceValue.File source)
)

/// <summary>Create an ImageCell widget with a text and an image source</summary>
/// <param name="text">The text of the cell</param>
/// <param name="source">The image source</param>
static member inline ImageCell<'msg>(text: string, source: Uri) =
View.ImageCell<'msg>(text, ImageSource.FromUri(source))
WidgetBuilder<'msg, IFabImageCell>(ImageCell.WidgetKey, TextCell.Text.WithValue(text), ImageCell.ImageSource.WithValue(ImageSourceValue.Uri source))

/// <summary>Create an ImageCell widget with a text and an image source</summary>
/// <param name="text">The text of the cell</param>
/// <param name="source">The image source</param>
static member inline ImageCell<'msg>(text: string, source: Stream) =
View.ImageCell<'msg>(text, ImageSource.FromStream(fun () -> source))
WidgetBuilder<'msg, IFabImageCell>(
ImageCell.WidgetKey,
TextCell.Text.WithValue(text),
ImageCell.ImageSource.WithValue(ImageSourceValue.Stream source)
)

[<Extension>]
type ImageCellModifiers =
Expand Down
22 changes: 11 additions & 11 deletions src/Fabulous.MauiControls/Views/Controls/Button.fs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ module Button =

let FontSize = Attributes.defineBindableFloat Button.FontSizeProperty

let ImageSource =
Attributes.defineBindableWithEquality<ImageSource> Button.ImageSourceProperty
let ImageSource = Attributes.defineBindableImageSource Button.ImageSourceProperty

let LineBreakMode =
Attributes.defineBindableWithEquality<LineBreakMode> Button.LineBreakModeProperty
Expand Down Expand Up @@ -156,7 +155,7 @@ type ButtonModifiers =
/// <param name="source">The image source</param>
[<Extension>]
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, source: ImageSource) =
this.AddScalar(Button.ImageSource.WithValue(source))
this.AddScalar(Button.ImageSource.WithValue(ImageSourceValue.Source source))

/// <summary>Set the line break mode</summary>
/// <param name="this">Current widget</param>
Expand Down Expand Up @@ -211,23 +210,24 @@ type ButtonModifiers =
type ButtonExtraModifiers =
/// <summary>Set the image source</summary>
/// <param name="this">Current widget</param>
/// <param name="source">The image source</param>
/// <param name="value">The image source</param>
[<Extension>]
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, source: string) =
this.image(ImageSource.FromFile(source))
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, value: string) =
this.AddScalar(Button.ImageSource.WithValue(ImageSourceValue.File value))

/// <summary>Set the image source</summary>
/// <param name="this">Current widget</param>
/// <param name="source">The image source</param>
/// <param name="value">The image source</param>
[<Extension>]
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, source: Uri) = this.image(ImageSource.FromUri(source))
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, value: Uri) =
this.AddScalar(Button.ImageSource.WithValue(ImageSourceValue.Uri value))

/// <summary>Set the image source</summary>
/// <param name="this">Current widget</param>
/// <param name="source">The image source</param>
/// <param name="value">The image source</param>
[<Extension>]
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, source: Stream) =
this.image(ImageSource.FromStream(fun () -> source))
static member inline image(this: WidgetBuilder<'msg, #IFabButton>, value: Stream) =
this.AddScalar(Button.ImageSource.WithValue(ImageSourceValue.Stream value))

/// <summary>Set the padding inside the button</summary>
/// <param name="this">Current widget</param>
Expand Down
35 changes: 9 additions & 26 deletions src/Fabulous.MauiControls/Views/Controls/Image.fs
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,7 @@ module Image =

let IsOpaque = Attributes.defineBindableBool Image.IsOpaqueProperty

let Source = Attributes.defineBindableWithEquality<ImageSource> Image.SourceProperty

/// Performance optimization: avoid allocating a new ImageSource instance on each update
/// we store the user value (eg. string, Uri, Stream) and convert it to an ImageSource only when needed
let inline private defineSourceAttribute<'model when 'model: equality> ([<InlineIfLambda>] convertModelToValue: 'model -> ImageSource) =
Attributes.defineScalar<'model, 'model> Image.SourceProperty.PropertyName id ScalarAttributeComparers.equalityCompare (fun _ newValueOpt node ->
let target = node.Target :?> Image

match newValueOpt with
| ValueNone -> target.ClearValue(Image.SourceProperty)
| ValueSome v -> target.SetValue(Image.SourceProperty, convertModelToValue v))

let SourceFile = defineSourceAttribute<string> ImageSource.FromFile

let SourceUri = defineSourceAttribute<Uri> ImageSource.FromUri

let SourceStream =
defineSourceAttribute<Stream>(fun stream -> ImageSource.FromStream(fun () -> stream))
let Source = Attributes.defineBindableImageSource Image.SourceProperty

[<AutoOpen>]
module ImageBuilders =
Expand All @@ -48,46 +31,46 @@ module ImageBuilders =
/// <summary>Create an Image widget with a source</summary>
/// <param name="source">The image source</param>
static member inline Image<'msg>(source: ImageSource) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(source))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Source source))

/// <summary>Create an Image widget with a source and an aspect</summary>
/// <param name="source">The image source</param>
/// <param name="aspect">The image aspect</param>
static member inline Image<'msg>(source: ImageSource, aspect: Aspect) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(source), Image.Aspect.WithValue(aspect))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Source source), Image.Aspect.WithValue(aspect))

/// <summary>Create an Image widget with a source</summary>
/// <param name="source">The image source</param>
static member inline Image<'msg>(source: string) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceFile.WithValue(source))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.File source))

/// <summary>Create an Image widget with a source and an aspect</summary>
/// <param name="source">The image source</param>
/// <param name="aspect">The image aspect</param>
static member inline Image<'msg>(source: string, aspect: Aspect) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceFile.WithValue(source), Image.Aspect.WithValue(aspect))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.File source), Image.Aspect.WithValue(aspect))

/// <summary>Create an Image widget with a source</summary>
/// <param name="source">The image source</param>
static member inline Image<'msg>(source: Uri) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceUri.WithValue(source))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Uri source))

/// <summary>Create an Image widget with a source and an aspect</summary>
/// <param name="source">The image source</param>
/// <param name="aspect">The image aspect</param>
static member inline Image<'msg>(source: Uri, aspect: Aspect) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceUri.WithValue(source), Image.Aspect.WithValue(aspect))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Uri source), Image.Aspect.WithValue(aspect))

/// <summary>Create an Image widget with a source</summary>
/// <param name="source">The image source</param>
static member inline Image<'msg>(source: Stream) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceStream.WithValue(source))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Stream source))

/// <summary>Create an Image widget with a source and an aspect</summary>
/// <param name="source">The image source</param>
/// <param name="aspect">The image aspect</param>
static member inline Image<'msg>(source: Stream, aspect: Aspect) =
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.SourceStream.WithValue(source), Image.Aspect.WithValue(aspect))
WidgetBuilder<'msg, IFabImage>(Image.WidgetKey, Image.Source.WithValue(ImageSourceValue.Stream source), Image.Aspect.WithValue(aspect))

[<Extension>]
type ImageModifiers =
Expand Down
Loading

0 comments on commit 67e487a

Please sign in to comment.