├── global.json ├── demo ├── ios │ ├── Resources │ │ ├── Default.png │ │ ├── Default@2x.png │ │ ├── Default-568h@2x.png │ │ ├── Default-Portrait.png │ │ └── Default-Portrait@2x.png │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Icon120.png │ │ │ ├── Icon152.png │ │ │ ├── Icon167.png │ │ │ ├── Icon180.png │ │ │ ├── Icon20.png │ │ │ ├── Icon29.png │ │ │ ├── Icon40.png │ │ │ ├── Icon58.png │ │ │ ├── Icon60.png │ │ │ ├── Icon76.png │ │ │ ├── Icon80.png │ │ │ ├── Icon87.png │ │ │ ├── Icon1024.png │ │ │ └── Contents.json │ ├── Entitlements.plist │ ├── Main.cs │ ├── AppDelegate.cs │ ├── Info.plist │ └── Properties │ │ └── AssemblyInfo.cs ├── app │ ├── DIN Condensed Bold.ttf │ ├── Font Awesome 5 Free-Solid-900.otf │ ├── WebViewPage.cs │ ├── GlobalUsings.cs │ ├── AbsoluteLayoutPage.cs │ ├── Demo.csproj │ ├── Counter.cs │ ├── FormattedStringPage.cs │ ├── Shapes.cs │ ├── EntryAndEditor.cs │ ├── Timer.cs │ ├── Brushes.cs │ ├── BehaviorPage.cs │ ├── SwipeViewPage.cs │ ├── DynamicGrid.cs │ ├── GroupedCollectionView.cs │ └── DancingBars.cs └── droid │ ├── Resources │ ├── mipmap-hdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-mdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-anydpi-v26 │ │ ├── icon.xml │ │ └── icon_round.xml │ ├── values │ │ ├── colors.xml │ │ └── styles.xml │ └── layout │ │ ├── Toolbar.xml │ │ └── Tabbar.xml │ ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs │ └── MainActivity.cs ├── assets └── flow-with-middleware.png ├── codegen ├── src │ ├── attributes │ │ ├── Interfaces.cs │ │ ├── CodeGen.Attributes.csproj │ │ └── Attributes.cs │ ├── generators │ │ └── CodeGen.Generators.csproj │ └── metapackage │ │ └── metapackage.csproj ├── pack ├── Directory.Build.props ├── test │ ├── CodeGen.Tests.csproj │ ├── SignalTests.cs │ ├── RecordTests.cs │ └── UnionTests.cs ├── README.md └── CodeGen.sln ├── maps ├── demo │ ├── ios │ │ ├── Resources │ │ │ ├── Default.png │ │ │ ├── Default@2x.png │ │ │ ├── Default-568h@2x.png │ │ │ ├── Default-Portrait.png │ │ │ └── Default-Portrait@2x.png │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon120.png │ │ │ │ ├── Icon152.png │ │ │ │ ├── Icon167.png │ │ │ │ ├── Icon180.png │ │ │ │ ├── Icon20.png │ │ │ │ ├── Icon29.png │ │ │ │ ├── Icon40.png │ │ │ │ ├── Icon58.png │ │ │ │ ├── Icon60.png │ │ │ │ ├── Icon76.png │ │ │ │ ├── Icon80.png │ │ │ │ ├── Icon87.png │ │ │ │ ├── Icon1024.png │ │ │ │ └── Contents.json │ │ ├── Entitlements.plist │ │ ├── Main.cs │ │ ├── AppDelegate.cs │ │ └── Info.plist │ ├── app │ │ ├── Font Awesome 5 Free-Solid-900.otf │ │ ├── Maps.Demo.csproj │ │ ├── SignalsStateReducers.cs │ │ └── CityLoader.cs │ └── droid │ │ ├── Resources │ │ ├── mipmap-hdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-mdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-anydpi-v26 │ │ │ ├── icon.xml │ │ │ └── icon_round.xml │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── layout │ │ │ ├── Toolbar.xml │ │ │ └── Tabbar.xml │ │ ├── Properties │ │ └── AndroidManifest.xml │ │ └── MainActivity.cs ├── src │ ├── MapElement.cs │ ├── Laconic.Maps.csproj │ ├── Polygon.cs │ ├── Pin.cs │ ├── Position.cs │ ├── Map.cs │ ├── Distance.cs │ └── MapSpan.cs └── test │ ├── MapTests.csproj │ └── MapTests.cs ├── cli-template ├── src │ ├── ios │ │ ├── Resources │ │ │ ├── Default.png │ │ │ ├── Default@2x.png │ │ │ ├── Default-568h@2x.png │ │ │ ├── Default-Portrait.png │ │ │ ├── Default-Portrait@2x.png │ │ │ └── LaunchScreen.storyboard │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon1024.png │ │ │ │ ├── Icon120.png │ │ │ │ ├── Icon152.png │ │ │ │ ├── Icon167.png │ │ │ │ ├── Icon180.png │ │ │ │ ├── Icon20.png │ │ │ │ ├── Icon29.png │ │ │ │ ├── Icon40.png │ │ │ │ ├── Icon58.png │ │ │ │ ├── Icon60.png │ │ │ │ ├── Icon76.png │ │ │ │ ├── Icon80.png │ │ │ │ ├── Icon87.png │ │ │ │ └── Contents.json │ │ ├── Entitlements.plist │ │ ├── AppDelegate.cs │ │ └── Info.plist │ ├── droid │ │ ├── Resources │ │ │ ├── mipmap-hdpi │ │ │ │ ├── icon.png │ │ │ │ └── launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── icon.png │ │ │ │ └── launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── icon.png │ │ │ │ └── launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── icon.png │ │ │ │ └── launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── icon.png │ │ │ │ └── launcher_foreground.png │ │ │ └── layout │ │ │ │ ├── Toolbar.xml │ │ │ │ └── Tabbar.xml │ │ ├── Properties │ │ │ └── AndroidManifest.xml │ │ └── MainActivity.cs │ ├── Laconic.Template.sln.DotSettings │ ├── app │ │ ├── Laconic.Template.csproj │ │ └── App.cs │ └── .template.config │ │ └── template.json └── Package.csproj ├── src ├── GlobalUsings.cs ├── Laconic.csproj ├── View.cs ├── EventInfo.cs ├── GestureRecognizers.cs ├── Signal.cs ├── AbsoluteLayoutDiff.cs ├── ToViewListExtensions.cs ├── VisualElement.cs ├── FormattedString.cs ├── TabbedPage.cs ├── Behaviors.cs ├── ImageSource.cs ├── GridViewList.cs ├── Key.cs ├── Pages.cs ├── GridDiff.cs ├── Size.cs ├── Thickness.cs ├── Point.cs ├── ElementListDiff.cs ├── CornerRadius.cs ├── ViewList.cs ├── ItemsViews.cs ├── Controls.cs ├── ElementList.cs ├── DiffOperations.cs ├── ContextExpander.cs └── ViewListDiff.cs ├── test ├── GlobalUsings.cs ├── PickerTests.cs ├── LaconicTests.csproj ├── StackLayoutTests.cs ├── ImageSourceTests.cs ├── ShapeTests.cs ├── BasicTests.cs ├── MiddlewareTests.cs ├── GridTests.cs ├── BehaviorTests.cs ├── CollectionViewTests.cs └── BinderTests.cs ├── .gitignore ├── tools └── codegen-controls │ └── CodeGen.Controls.csproj ├── Directory.build.props ├── Laconic.sln.DotSettings ├── LICENSE └── binding-report.md /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.202" 4 | } 5 | } -------------------------------------------------------------------------------- /demo/ios/Resources/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Resources/Default.png -------------------------------------------------------------------------------- /assets/flow-with-middleware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/assets/flow-with-middleware.png -------------------------------------------------------------------------------- /codegen/src/attributes/Interfaces.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.CodeGeneration 2 | { 3 | public interface record {} 4 | } -------------------------------------------------------------------------------- /demo/app/DIN Condensed Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/app/DIN Condensed Bold.ttf -------------------------------------------------------------------------------- /demo/ios/Resources/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Resources/Default@2x.png -------------------------------------------------------------------------------- /maps/demo/ios/Resources/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Resources/Default.png -------------------------------------------------------------------------------- /demo/ios/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /maps/demo/ios/Resources/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Resources/Default@2x.png -------------------------------------------------------------------------------- /demo/ios/Resources/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Resources/Default-Portrait.png -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Resources/Default.png -------------------------------------------------------------------------------- /demo/app/Font Awesome 5 Free-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/app/Font Awesome 5 Free-Solid-900.otf -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /demo/ios/Resources/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Resources/Default-Portrait@2x.png -------------------------------------------------------------------------------- /maps/demo/ios/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Resources/Default@2x.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /maps/demo/ios/Resources/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Resources/Default-Portrait.png -------------------------------------------------------------------------------- /maps/demo/app/Font Awesome 5 Free-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/app/Font Awesome 5 Free-Solid-900.otf -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /maps/demo/ios/Resources/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Resources/Default-Portrait@2x.png -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Resources/Default-Portrait.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Resources/Default-Portrait@2x.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png -------------------------------------------------------------------------------- /codegen/pack: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dotnet pack src/generators -o ./packages 4 | dotnet pack src/attributes -o ./packages 5 | dotnet pack src/metapackage -o ./packages 6 | -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/demo/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png -------------------------------------------------------------------------------- /demo/app/WebViewPage.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class WebViewPage 4 | { 5 | public static WebView Content() => new() { Source = "https://google.com" }; 6 | } -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/maps/demo/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon1024.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon120.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon152.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon167.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon180.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon20.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon58.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon60.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon80.png -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Icon87.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /demo/app/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System; 4 | global using System.Collections.Generic; 5 | global using System.Linq; 6 | global using xf = Xamarin.Forms; -------------------------------------------------------------------------------- /src/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections; 3 | global using System.Collections.Generic; 4 | global using System.Linq; 5 | global using xf = Xamarin.Forms; 6 | -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shirshov/laconic/HEAD/cli-template/src/droid/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using System; 4 | global using System.Collections.Generic; 5 | global using System.Linq; 6 | global using Shouldly; 7 | global using Xunit; 8 | global using xf = Xamarin.Forms; -------------------------------------------------------------------------------- /demo/ios/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /maps/demo/ios/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /cli-template/src/ios/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | pkg 4 | packages 5 | artifacts 6 | redist 7 | .DS_Store 8 | .vs 9 | .idea 10 | xcuserdata 11 | Resource.Designer.cs 12 | *.apk 13 | *.nupkg 14 | *.pidb 15 | *.userprefs 16 | *.suo 17 | *.user 18 | *.targets.overrides 19 | *.orig 20 | .fake 21 | .ionide -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-anydpi-v26/icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /demo/droid/Resources/mipmap-anydpi-v26/icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-anydpi-v26/icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/mipmap-anydpi-v26/icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /demo/droid/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 5 | #3F51B5 7 | #303F9F 9 | #FF4081 11 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 5 | #3F51B5 7 | #303F9F 9 | #FF4081 11 | -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/layout/Toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /codegen/src/generators/CodeGen.Generators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | Laconic.CodeGen.Generators 6 | Laconic.CodeGen.Generators 7 | 8.0 8 | 9 | 10 | -------------------------------------------------------------------------------- /tools/codegen-controls/CodeGen.Controls.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | 10 6 | Laconic.CodeGen 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Laconic.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 10 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/droid/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /maps/demo/droid/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/ios/Main.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace Laconic.Demo 4 | { 5 | public class Application 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, "AppDelegate"); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /maps/demo/ios/Main.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace Laconic.Demo 4 | { 5 | public class Application 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, "AppDelegate"); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /demo/droid/Resources/layout/Toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/layout/Toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /codegen/src/attributes/CodeGen.Attributes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | Laconic.CodeGen.Attributes 7 | Laconic.CodeGen.Attributes 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /cli-template/src/droid/Resources/layout/Tabbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5.0.0.2401-pre1 4 | Alex Shirshov 5 | MVU library for Xamarin.Forms 6 | Write your Model-View-Update code for Xamarin.Forms in React+Redux fashion 7 | Copyright (c) Alex Shirshov. All rights reserved. 8 | MIT 9 | https://github.com/shirshov/laconic 10 | 11 | 12 | -------------------------------------------------------------------------------- /maps/demo/ios/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using UIKit; 3 | 4 | namespace Laconic.Maps.Demo 5 | { 6 | [Register("AppDelegate")] 7 | public class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 8 | { 9 | public override bool FinishedLaunching(UIApplication app, NSDictionary options) 10 | { 11 | Xamarin.Forms.Forms.Init(); 12 | Xamarin.FormsMaps.Init(); 13 | 14 | LoadApplication(new MapsApp()); 15 | 16 | return base.FinishedLaunching(app, options); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/PickerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class PickerTests 4 | { 5 | [Fact] 6 | public void do_not_update_Items_until_changed() 7 | { 8 | var binder = Binder.Create(0, (s, g) => 1); 9 | var picker = binder.CreateElement(s => new Picker 10 | { 11 | Items = new [] {"0", "1", "2"}, 12 | SelectedIndex = s, 13 | SelectedIndexChanged = e => new Signal(e) 14 | }); 15 | 16 | picker.SelectedIndex = 1; 17 | 18 | picker.SelectedIndex.ShouldBe(1); 19 | } 20 | } -------------------------------------------------------------------------------- /cli-template/src/droid/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /demo/droid/Resources/layout/Tabbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/layout/Tabbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/View.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public interface View 4 | { 5 | Dictionary GestureRecognizers { get; } 6 | } 7 | public abstract class View : VisualElement, View where T : xf.View, new() 8 | { 9 | public LayoutOptions HorizontalOptions { 10 | init => SetValue(xf.View.HorizontalOptionsProperty, value); 11 | } 12 | 13 | public LayoutOptions VerticalOptions { 14 | init => SetValue(xf.View.VerticalOptionsProperty, value); 15 | } 16 | 17 | public Thickness Margin { 18 | init => SetValue(xf.View.MarginProperty, value); 19 | } 20 | } -------------------------------------------------------------------------------- /codegen/src/metapackage/metapackage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netstandard2.0 7 | Laconic.CodeGeneration 8 | Laconic.CodeGeneration 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /cli-template/src/ios/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using UIKit; 3 | 4 | namespace Laconic.Template.iOS 5 | { 6 | [Register("AppDelegate")] 7 | public class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 8 | { 9 | public override bool FinishedLaunching(UIApplication app, NSDictionary options) 10 | { 11 | Xamarin.Forms.Forms.Init(); 12 | LoadApplication(new App()); 13 | 14 | return base.FinishedLaunching(app, options); 15 | } 16 | 17 | static void Main(string[] args) => UIApplication.Main(args, null, "AppDelegate"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/EventInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class EventInfo 4 | { 5 | public readonly Func SignalMaker; 6 | public readonly Action Subscribe; 7 | public readonly Action Unsubscribe; 8 | 9 | public EventInfo(Func signalMaker, 10 | Action subscribe, 11 | Action unsubscribe) 12 | { 13 | SignalMaker = signalMaker; 14 | Subscribe = subscribe; 15 | Unsubscribe = unsubscribe; 16 | } 17 | } -------------------------------------------------------------------------------- /maps/src/MapElement.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Maps 2 | { 3 | public abstract class MapElement : Element where T : Xamarin.Forms.BindableObject 4 | { 5 | public Color StrokeColor 6 | { 7 | get => GetValue(Xamarin.Forms.Maps.MapElement.StrokeColorProperty); 8 | set => SetValue(Xamarin.Forms.Maps.MapElement.StrokeColorProperty, value); 9 | } 10 | 11 | public float StrokeWidth 12 | { 13 | get => GetValue(Xamarin.Forms.Maps.MapElement.StrokeWidthProperty); 14 | set => SetValue(Xamarin.Forms.Maps.MapElement.StrokeWidthProperty, value); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Laconic.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 6 | /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/MSBuild.dll 7 | 4294967294 8 | -------------------------------------------------------------------------------- /codegen/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.9.3 4 | 0.9.3-beta 5 | Alex Shirshov 6 | Records and Unions for C# 8 7 | Compile-time code generation of records and discriminated unions for C# 8 8 | Copyright (c) Alex Shirshov. All rights reserved. 9 | MIT 10 | https://github.com/shirshov/laconic/tree/master/codegen 11 | 12 | -------------------------------------------------------------------------------- /maps/src/Laconic.Maps.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 9 6 | enable 7 | Binding for Xamarin.Forms.Maps for use with Laconic 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/GestureRecognizers.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public interface IGestureRecognizer 4 | { 5 | } 6 | 7 | public class TapGestureRecognizer : Element, IGestureRecognizer 8 | { 9 | public int NumberOfTapsRequired 10 | { 11 | set => SetValue(xf.TapGestureRecognizer.NumberOfTapsRequiredProperty, value); 12 | } 13 | 14 | public System.Func Tapped 15 | { 16 | set => SetEvent(nameof(Tapped), value, 17 | (ctl, handler) => ctl.Tapped += handler, 18 | (ctl, handler) => ctl.Tapped -= handler); 19 | } 20 | 21 | protected internal override xf.BindableObject CreateView() => new xf.TapGestureRecognizer(); 22 | } -------------------------------------------------------------------------------- /cli-template/src/Laconic.Template.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 6 | /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/MSBuild.dll 7 | 4294967294 8 | -------------------------------------------------------------------------------- /src/Signal.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class Signal 4 | { 5 | public readonly object? Payload; 6 | readonly object? _p1; 7 | readonly object? _p2; 8 | 9 | public Signal(object? payload) => (Payload, _p1, _p2) = (payload, payload, null); 10 | public Signal(object p1, object? p2) => (Payload, _p1, _p2) = (p1, p1, p2); 11 | 12 | public void Deconstruct(out object? p1, out object? p2) => (p1, p2) = (_p1, _p2); 13 | 14 | public override string ToString() => $"{GetType()}: {_p1} {_p2}"; 15 | } 16 | 17 | public class Signal : Signal 18 | { 19 | public Signal(T payload) : base(payload) 20 | { 21 | } 22 | 23 | public new T Payload => (T) base.Payload!; 24 | } -------------------------------------------------------------------------------- /src/AbsoluteLayoutDiff.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | static class AbsoluteLayoutDiff 4 | { 5 | public static IEnumerable Calculate( 6 | Key key, AbsoluteLayoutViewList? existingList, AbsoluteLayoutViewList newList) 7 | { 8 | var existingPos = new AbsLayoutInfo(new Bounds(-1, -1, -1, -1), AbsoluteLayoutFlags.None); 9 | if (existingList != null && existingList.ContainsKey(key)) 10 | existingPos = existingList.GetPositioning(key); 11 | 12 | var newPos = newList.GetPositioning(key); 13 | 14 | if (newPos != existingPos) 15 | yield return new SetAbsoluteLayoutPositioning(newPos.Bounds, newPos.Flags); 16 | } 17 | } -------------------------------------------------------------------------------- /cli-template/src/app/Laconic.Template.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | true 6 | 7 | 8 | 9 | portable 10 | true 11 | 9.0 12 | 13 | 14 | 15 | 9.0 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/app/AbsoluteLayoutPage.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class AbsoluteLayoutPage 4 | { 5 | public static AbsoluteLayout Content() => new() { 6 | ["b", (0.5, 0, 100, 25), AbsoluteLayoutFlags.PositionProportional] = new BoxView { Color = Color.Blue}, 7 | ["g", (0 ,0.5, 25, 100), AbsoluteLayoutFlags.PositionProportional] = new BoxView { Color = Color.Green}, 8 | ["r", (1, 0.5,25,100), AbsoluteLayoutFlags.PositionProportional] = new BoxView { Color = Color.Red}, 9 | ["k", (0.5,1,100,25), AbsoluteLayoutFlags.PositionProportional] = new BoxView { Color = Color.Black}, 10 | ["t", (0.5, 0.5, 110, 25), AbsoluteLayoutFlags.PositionProportional] = new Label { 11 | Text = "Centered text", 12 | } 13 | }; 14 | } -------------------------------------------------------------------------------- /maps/demo/app/Maps.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 9 6 | 7 | 8 | 9 | portable 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /codegen/test/CodeGen.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.1 4 | 8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cli-template/Package.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Template 5 | 1.0.3 6 | Laconic.AppTemplate 7 | Laconic App Template 8 | Alex Shirshov 9 | Template for creating Xamarin.Forms + Laconic apps 10 | laconic;xamarin.forms 11 | 12 | netcoreapp3.1 13 | 14 | false 15 | $(NoWarn);NU5118;NU5119;NU5128 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /codegen/src/attributes/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CodeGeneration.Roslyn; 3 | 4 | namespace Laconic.CodeGeneration 5 | { 6 | [AttributeUsage(AttributeTargets.Interface)] 7 | [CodeGenerationAttribute("Laconic.CodeGeneration.UnionGenerator, Laconic.CodeGen.Generators")] 8 | public class UnionAttribute : Attribute 9 | { 10 | } 11 | 12 | [AttributeUsage(AttributeTargets.Interface)] 13 | [CodeGenerationAttribute("Laconic.CodeGeneration.RecordsGenerator, Laconic.CodeGen.Generators")] 14 | public class RecordsAttribute : Attribute 15 | { 16 | } 17 | 18 | [AttributeUsage(AttributeTargets.Interface)] 19 | [CodeGenerationAttribute("Laconic.CodeGeneration.SignalsGenerator, Laconic.CodeGen.Generators")] 20 | public class SignalsAttribute : Attribute 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/droid/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace Laconic.Demo 6 | { 7 | [Activity(Label = "Laconic Demo", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, 8 | ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 9 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 10 | { 11 | protected override void OnCreate(Bundle savedInstanceState) 12 | { 13 | TabLayoutResource = Resource.Layout.Tabbar; 14 | ToolbarResource = Resource.Layout.Toolbar; 15 | 16 | base.OnCreate(savedInstanceState); 17 | 18 | Xamarin.Forms.Forms.Init(this, savedInstanceState); 19 | LoadApplication(new App()); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/LaconicTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 10 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /maps/demo/droid/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace Laconic.Maps.Demo 6 | { 7 | [Activity(Label = "Laconic.Maps Demo", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, 8 | ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 9 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 10 | { 11 | protected override void OnCreate(Bundle savedInstanceState) 12 | { 13 | TabLayoutResource = Resource.Layout.Tabbar; 14 | ToolbarResource = Resource.Layout.Toolbar; 15 | 16 | base.OnCreate(savedInstanceState); 17 | 18 | Xamarin.Forms.Forms.Init(this, savedInstanceState); 19 | LoadApplication(new App()); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /maps/src/Polygon.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Laconic.Maps 4 | { 5 | public class Polygon : MapElement 6 | { 7 | protected override Xamarin.Forms.BindableObject CreateView() => new Xamarin.Forms.Maps.Polygon(); 8 | 9 | public Color FillColor 10 | { 11 | get => GetValue(Xamarin.Forms.Maps.Polygon.FillColorProperty); 12 | set => SetValue(Xamarin.Forms.Maps.Polygon.FillColorProperty, value); 13 | } 14 | 15 | public List Geopath { 16 | set => SetValue(nameof(Geopath), value, polygon => { 17 | polygon.Geopath.Clear(); 18 | foreach (var x in value) 19 | polygon.Geopath.Add(new Xamarin.Forms.Maps.Position(x.Latitude, x.Longitude)); 20 | }); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /demo/app/Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | true 6 | 10 7 | enable 8 | 9 | 10 | 11 | portable 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /maps/src/Pin.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Maps 2 | { 3 | public class Pin : Element 4 | { 5 | public PinType Type 6 | { 7 | set => SetValue(Xamarin.Forms.Maps.Pin.TypeProperty, (Xamarin.Forms.Maps.PinType)value); 8 | } 9 | 10 | public Position Position 11 | { 12 | set => SetValue(Xamarin.Forms.Maps.Pin.PositionProperty, new Xamarin.Forms.Maps.Position(value.Latitude, value.Longitude)); 13 | } 14 | 15 | public string Label 16 | { 17 | set => SetValue(Xamarin.Forms.Maps.Pin.LabelProperty, value); 18 | } 19 | 20 | public string Address 21 | { 22 | set => SetValue(Xamarin.Forms.Maps.Pin.AddressProperty, value); 23 | } 24 | 25 | protected override Xamarin.Forms.BindableObject CreateView() => new Xamarin.Forms.Maps.Pin(); 26 | } 27 | } -------------------------------------------------------------------------------- /cli-template/src/droid/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace Laconic.Template.Droid 6 | { 7 | [Activity(Label = "Laconic.Template", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] 8 | public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity 9 | { 10 | protected override void OnCreate(Bundle savedInstanceState) 11 | { 12 | TabLayoutResource = Resource.Layout.Tabbar; 13 | ToolbarResource = Resource.Layout.Toolbar; 14 | 15 | base.OnCreate(savedInstanceState); 16 | 17 | Xamarin.Forms.Forms.Init(this, savedInstanceState); 18 | LoadApplication(new App()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /demo/app/Counter.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class Counter 4 | { 5 | public static StackLayout Content(int state) => new() { 6 | Padding = 50, 7 | ["lbl"] = new Label 8 | { 9 | Text = $"You clicked {state} times", 10 | FontSize = 30, 11 | FontAttributes = FontAttributes.Bold, 12 | VerticalOptions = LayoutOptions.CenterAndExpand, 13 | HorizontalOptions = LayoutOptions.Center 14 | }, 15 | ["btn"] = new Button 16 | { 17 | Text = "Click Me", 18 | Clicked = () => new Signal("inc"), 19 | TextColor = Color.White, 20 | FontSize = 20, 21 | BackgroundColor = Color.Coral, 22 | BorderColor = Color.Chocolate, 23 | BorderWidth = 2, 24 | CornerRadius = 20, 25 | HorizontalOptions = LayoutOptions.Center, 26 | Padding = (30, 0) 27 | } 28 | }; 29 | } -------------------------------------------------------------------------------- /demo/app/FormattedStringPage.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class FormattedStringPage 4 | { 5 | public static Label Content() => new() { 6 | Margin = (30, 30), 7 | VerticalOptions = LayoutOptions.Center, 8 | FormattedText = new FormattedString { 9 | new Span {Text = "As ", FontFamily = "AmericanTypewriter" }, // the font is iOS only 10 | new Span {Text = "you ", FontSize = 30}, 11 | new Span {Text = "value ", FontAttributes = FontAttributes.Bold | FontAttributes.Italic}, 12 | new Span {Text = "your life or ",BackgroundColor = Color.Yellow }, 13 | new Span {Text = "your ", CharacterSpacing = 10}, 14 | new Span {Text = "reason ", LineHeight = 15}, 15 | new Span {Text = "keep away ", TextColor = Color.Blue}, 16 | new Span {Text = "from the ", TextDecorations = TextDecorations.Underline, }, 17 | "moor" // plain text is allowed 18 | } 19 | }; 20 | } -------------------------------------------------------------------------------- /test/StackLayoutTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class StackLayoutTests 4 | { 5 | [Fact] 6 | public void can_add_children() 7 | { 8 | var sl = new StackLayout { 9 | [1] = new Label { Text = "lbl1"}, 10 | [2] = new Label { Text = "lbl2" } 11 | }; 12 | 13 | sl.Children.Count().ShouldBe(2); 14 | 15 | var binder = Binder.Create("state", (state, _) => state); 16 | var real = binder.CreateElement(state => new StackLayout { 17 | ["1"] = new Label { Text = "lbl1"}, 18 | ["2"] = new Label { Text = "lbl2" } 19 | }); 20 | 21 | real.Children.Count.ShouldBe(2); 22 | real.Children[0].ShouldBeOfType(); 23 | real.Children[1].ShouldBeOfType(); 24 | } 25 | 26 | [Fact] 27 | public void children_from_LINQ() 28 | { 29 | var s = new StackLayout { 30 | Children = Enumerable.Range(1, 5).ToViewList(i => i, i => new Label {Text = "Item " + i}) 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/ToViewListExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public static class ToViewListExtensions 4 | { 5 | public static Dictionary ToViewList(this IEnumerable source, 6 | Func keySelector, Func itemSelector) => 7 | source.ToDictionary(keySelector, itemSelector); 8 | 9 | public static Dictionary<(Key, int Row, int Column), View> ToGridViewList(this IEnumerable source, 10 | Func keySelector, Func itemSelector) => 11 | source.ToDictionary(keySelector, itemSelector); 12 | 13 | public static ItemsViewList ToItemsList(this IEnumerable source, 14 | Func reuseKeySelector, Func keySelector, Func viewSelector) 15 | { 16 | var res = new ItemsViewList(); 17 | foreach (var item in source) 18 | { 19 | var key = keySelector(item); 20 | res.Add(key, viewSelector(item)); 21 | res.ReuseKeys[key] = reuseKeySelector(item); 22 | } 23 | 24 | return res; 25 | } 26 | } -------------------------------------------------------------------------------- /cli-template/src/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Alex Shirshov", 4 | "name": "Laconic Mobile App", 5 | "identity": "Laconic.AppTemplate", 6 | "description": "Xamarin-based mobile app template using React + Redux like approach", 7 | "shortName": "laconicapp", 8 | "classifications": [ 9 | "Xamarin.Forms", 10 | "Laconic" 11 | ], 12 | "tags": { 13 | "language": "C#", 14 | "type": "project" 15 | }, 16 | "sourceName": "Laconic.Template", 17 | "preferNameDirectory": true, 18 | "guids": [ 19 | "{A4EE6D41-06D6-41B6-85C5-C53BFFBB4896}", 20 | "{BD92F4FD-91AE-48EB-8721-6FCAA849717B}", 21 | "{ECF3B85F-6039-4671-B95E-EAB7E3A70EB1}" 22 | ], 23 | "symbols": { 24 | "organizationIdentifier": { 25 | "type": "parameter", 26 | "description": "Organization Identifier for bundle ID", 27 | "defaultValue": "com.companyname", 28 | "replaces": "com.companyname" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/VisualElement.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public abstract partial class VisualElement : Element where T : xf.VisualElement, new() 4 | { 5 | // TODO: why is it here, and not on Element? 6 | protected internal override xf.BindableObject CreateView() => new T(); 7 | 8 | public Dictionary GestureRecognizers { get; } = new(); 9 | 10 | public VisualElement() => ElementLists.Add(nameof(Behaviors), element => (IList)element.Behaviors); 11 | 12 | public VisualMarker Visual 13 | { 14 | get => GetValue(xf.VisualElement.VisualProperty); 15 | set => SetValue(xf.VisualElement.VisualProperty, value); 16 | } 17 | 18 | public IBrush Background 19 | { 20 | get => GetValue(xf.VisualElement.BackgroundProperty); 21 | set => SetValue(xf.VisualElement.BackgroundProperty, value); 22 | } 23 | 24 | public ElementList Behaviors { 25 | get => ElementLists[nameof(Behaviors)]; 26 | set => ElementLists[nameof(Behaviors)] = value; 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Shirshov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/app/Shapes.cs: -------------------------------------------------------------------------------- 1 | using Laconic.Shapes; 2 | 3 | namespace Laconic.Demo; 4 | 5 | static class Shapes 6 | { 7 | public static ScrollView Content() => new() { 8 | Padding = 20, 9 | Content = new StackLayout { 10 | ["line"] = new Line { 11 | X1 = 40, 12 | Y1 = 0, 13 | X2 = 0, 14 | Y2 = 120, 15 | Stroke = Brush.Red, 16 | StrokeThickness = 5 17 | }, 18 | ["rect"] = new Rectangle { 19 | Fill = Brush.Blue, 20 | Stroke = Brush.Black, 21 | StrokeThickness = 3, 22 | RadiusX = 50, 23 | RadiusY = 10, 24 | WidthRequest = 200, 25 | HeightRequest = 100, 26 | HorizontalOptions = LayoutOptions.Start 27 | }, 28 | ["path"] = new Path { 29 | Data = "M 10,100 C 100,0 200,200 300,100", 30 | Stroke = Brush.Black, 31 | StrokeThickness = 3, 32 | Aspect = Stretch.Uniform 33 | } 34 | } 35 | }; 36 | } -------------------------------------------------------------------------------- /maps/test/MapTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 8.0 6 | enable 7 | 8 | 9 | 10 | 9.0 11 | 12 | 13 | 9.0 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /demo/ios/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using UIKit; 3 | 4 | namespace Laconic.Demo 5 | { 6 | // The UIApplicationDelegate for the application. This class is responsible for launching the 7 | // User Interface of the application, as well as listening (and optionally responding) to 8 | // application events from iOS. 9 | [Register("AppDelegate")] 10 | public class AppDelegate : Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 11 | { 12 | // 13 | // This method is invoked when the application has loaded and is ready to run. In this 14 | // method you should instantiate the window, load the UI into it and then make the window 15 | // visible. 16 | // 17 | // You have 17 seconds to return from this method, or iOS will terminate your application. 18 | // 19 | public override bool FinishedLaunching(UIApplication app, NSDictionary options) 20 | { 21 | Xamarin.Forms.Forms.SetFlags("RadioButton_Experimental"); 22 | Xamarin.Forms.Forms.Init(); 23 | LoadApplication(new App()); 24 | 25 | return base.FinishedLaunching(app, options); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/FormattedString.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public partial class Span : Element 4 | { 5 | protected internal override xf.BindableObject CreateView() => throw new NotImplementedException(); 6 | } 7 | 8 | public class FormattedString : IConvert, IEnumerable 9 | { 10 | readonly List _spans = new(); 11 | 12 | public void Add(string text) => _spans.Add(new Span{Text = text}); 13 | 14 | public void Add(Span span) => _spans.Add(span); 15 | 16 | public IEnumerator GetEnumerator() => throw new NotImplementedException(); 17 | 18 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 19 | 20 | object IConvert.ToNative() 21 | { 22 | // TODO: this is very inefficient. Must plug FormattedString into diff/patch properly. 23 | var ret = new xf.FormattedString(); 24 | foreach (var span in _spans) { 25 | var newSpan = new xf.Span(); 26 | foreach (var p in span.ProvidedValues) 27 | if (p.Value != null) 28 | newSpan.SetValue(p.Key, Patch.ConvertToNative(p.Value)); 29 | ret.Spans.Add(newSpan); 30 | } 31 | 32 | return ret; 33 | } 34 | } -------------------------------------------------------------------------------- /demo/droid/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("Counter2.Android")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("Counter2.Android")] 14 | [assembly: AssemblyCopyright("Copyright © 2014")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | [assembly: AssemblyVersion("1.0.0.0")] 26 | [assembly: AssemblyFileVersion("1.0.0.0")] 27 | 28 | // Add some common permissions, these can be removed if not needed 29 | [assembly: UsesPermission(Android.Manifest.Permission.Internet)] 30 | [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)] 31 | -------------------------------------------------------------------------------- /test/ImageSourceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class ImageSourceTests 4 | { 5 | [Fact] 6 | public void FontImageSource_can_be_created_and_updated() 7 | { 8 | var bp1 = new Image { 9 | Source = new FontImageSource {FontFamily = "Arial", Glyph = "a", Size = 13, Color = Color.Red} 10 | }; 11 | var bp2 = new Image { 12 | Source = new FontImageSource {FontFamily = "Helvetica", Glyph = "h", Size = 15, Color = Color.Green} 13 | }; 14 | 15 | var binder = Binder.CreateForTest(0, (s, g) => s + 1); 16 | var img = binder.CreateElement(s => s == 0 ? bp1 : bp2); 17 | 18 | var imgSource = img.Source.ShouldBeOfType(); 19 | 20 | imgSource.FontFamily.ShouldBe("Arial"); 21 | imgSource.Glyph.ShouldBe("a"); 22 | imgSource.Size.ShouldBe(13); 23 | imgSource.Color.ShouldBe(xf.Color.Red); 24 | 25 | binder.Send(new Signal(null)); 26 | 27 | imgSource = img.Source.ShouldBeOfType(); 28 | 29 | imgSource.FontFamily.ShouldBe("Helvetica"); 30 | imgSource.Glyph.ShouldBe("h"); 31 | imgSource.Size.ShouldBe(15); 32 | imgSource.Color.ShouldBe(xf.Color.Green); 33 | } 34 | } -------------------------------------------------------------------------------- /demo/app/EntryAndEditor.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class EntryAndEditor 4 | { 5 | public static VisualElement Content() => Element.WithContext("entry", ctx => { 6 | var (text, setState) = ctx.UseLocalState(""); 7 | return new StackLayout { 8 | BackgroundColor = Color.Bisque, 9 | Padding = 20, 10 | Spacing = 20, 11 | ["entry label"] = new Label {Text = "Entry:"}, 12 | ["entry"] = 13 | new Entry { 14 | Text = text, Placeholder = "Type something", TextChanged = e => setState(e.NewTextValue) 15 | }, 16 | ["editor label"] = new Label {Text = "Editor:"}, 17 | ["editor"] = 18 | new Editor { 19 | Placeholder = "Type something", 20 | HeightRequest = 100, 21 | Text = text, 22 | TextChanged = e => setState(e.NewTextValue) 23 | }, 24 | ["Numbers label"] = new Label {Text = "Entry with numeric keyboard:"}, 25 | ["numbers entry"] = new Entry { 26 | Placeholder = "Type something", 27 | Keyboard = Keyboard.Numeric, 28 | Text = text, 29 | TextChanged = e => setState(e.NewTextValue) 30 | } 31 | }; 32 | }); 33 | } -------------------------------------------------------------------------------- /demo/app/Timer.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class Timer 4 | { 5 | static StackLayout View(TimeSpan elapsed, string buttonTitle, Func buttonAction) => new() { 6 | Spacing = 30, 7 | Padding = 30, 8 | ["display"] = new Label { 9 | Text = elapsed.TotalSeconds.ToString("0.0"), 10 | FontFamily = "DINBold", 11 | FontSize = 50, 12 | FontAttributes = FontAttributes.Bold, 13 | HorizontalOptions = LayoutOptions.Center 14 | }, 15 | ["button"] = new Button { 16 | Text = buttonTitle, 17 | TextColor = Color.White, 18 | FontSize = 20, 19 | Clicked = buttonAction, 20 | BackgroundColor = Color.Coral, 21 | BorderColor = Color.Chocolate, 22 | BorderWidth = 2, 23 | CornerRadius = 20, 24 | HorizontalOptions = LayoutOptions.Center, 25 | Padding = (30, 0) 26 | } 27 | }; 28 | 29 | public static VisualElement Content() => Element.WithContext("timer", ctx => { 30 | var timer = ctx.UseTimer(TimeSpan.FromMilliseconds(100), start: false); 31 | 32 | return View(timer.Elapsed, 33 | timer.IsRunning ? "Stop" : "Start", 34 | timer.IsRunning ? () => timer.Stop() : () => timer.Start() 35 | ); 36 | }); 37 | } -------------------------------------------------------------------------------- /maps/src/Position.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Laconic.Maps 4 | { 5 | public readonly struct Position 6 | { 7 | public Position(double latitude, double longitude) 8 | { 9 | Latitude = Math.Min(Math.Max(latitude, -90.0), 90.0); 10 | Longitude = Math.Min(Math.Max(longitude, -180.0), 180.0); 11 | } 12 | 13 | public double Latitude { get; } 14 | public double Longitude { get; } 15 | 16 | public override bool Equals(object obj) 17 | { 18 | if (obj == null || obj.GetType() != this.GetType()) 19 | return false; 20 | var position = (Position) obj; 21 | return this.Latitude == position.Latitude && this.Longitude == position.Longitude; 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | var num1 = Latitude; 27 | var num2 = num1.GetHashCode() * 397; 28 | num1 = Longitude; 29 | var hashCode = num1.GetHashCode(); 30 | return num2 ^ hashCode; 31 | } 32 | 33 | public static bool operator ==(Position left, Position right) => Equals(left, right); 34 | 35 | public static bool operator !=(Position left, Position right) => !Equals(left, right); 36 | 37 | public static implicit operator Position((double latitude, double longitude) value) => 38 | new Position(value.latitude, value.longitude); 39 | } 40 | } -------------------------------------------------------------------------------- /demo/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UISupportedInterfaceOrientations 11 | 12 | UIInterfaceOrientationPortrait 13 | UIInterfaceOrientationLandscapeLeft 14 | UIInterfaceOrientationLandscapeRight 15 | 16 | UISupportedInterfaceOrientations~ipad 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationPortraitUpsideDown 20 | UIInterfaceOrientationLandscapeLeft 21 | UIInterfaceOrientationLandscapeRight 22 | 23 | MinimumOSVersion 24 | 8.0 25 | CFBundleDisplayName 26 | Laconic Demo 27 | CFBundleIdentifier 28 | laconic.demo 29 | CFBundleVersion 30 | 1.0 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | CFBundleName 34 | Laconic.Demo 35 | XSAppIconAssets 36 | Assets.xcassets/AppIcon.appiconset 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo/ios/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Counter2.iOS")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Counter2.iOS")] 12 | [assembly: AssemblyCopyright("Copyright © 2014")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("72bdc44f-c588-44f3-b6df-9aace7daafdd")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /maps/demo/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UISupportedInterfaceOrientations 11 | 12 | UIInterfaceOrientationPortrait 13 | UIInterfaceOrientationLandscapeLeft 14 | UIInterfaceOrientationLandscapeRight 15 | 16 | UISupportedInterfaceOrientations~ipad 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationPortraitUpsideDown 20 | UIInterfaceOrientationLandscapeLeft 21 | UIInterfaceOrientationLandscapeRight 22 | 23 | MinimumOSVersion 24 | 8.0 25 | CFBundleDisplayName 26 | Laconic.Maps Demo 27 | CFBundleIdentifier 28 | laconic.maps.demo 29 | CFBundleVersion 30 | 1.0 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | CFBundleName 34 | Laconic.Maps.Demo 35 | XSAppIconAssets 36 | Assets.xcassets/AppIcon.appiconset 37 | 38 | 39 | -------------------------------------------------------------------------------- /cli-template/src/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UISupportedInterfaceOrientations 11 | 12 | UIInterfaceOrientationPortrait 13 | UIInterfaceOrientationLandscapeLeft 14 | UIInterfaceOrientationLandscapeRight 15 | 16 | UISupportedInterfaceOrientations~ipad 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationPortraitUpsideDown 20 | UIInterfaceOrientationLandscapeLeft 21 | UIInterfaceOrientationLandscapeRight 22 | 23 | MinimumOSVersion 24 | 8.0 25 | CFBundleDisplayName 26 | Laconic.Template 27 | CFBundleIdentifier 28 | com.companyname.Laconic.Template 29 | CFBundleVersion 30 | 1.0 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | CFBundleName 34 | Laconic.Template 35 | XSAppIconAssets 36 | Assets.xcassets/AppIcon.appiconset 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo/app/Brushes.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class Brushes 4 | { 5 | public static StackLayout Content() => new() { 6 | Padding = 50, 7 | ["solid"] = new Frame { 8 | Background = Brush.DarkBlue, 9 | BorderColor = Color.LightGray, 10 | HasShadow = true, 11 | CornerRadius = 12, 12 | HeightRequest = 120, 13 | WidthRequest = 120 14 | }, 15 | ["linear"] = new Frame { 16 | BorderColor = Color.LightGray, 17 | HasShadow = true, 18 | CornerRadius = 12, 19 | HeightRequest = 120, 20 | WidthRequest = 120, 21 | Background = new LinearGradientBrush { 22 | StartPoint = (0, 0), 23 | EndPoint = (1, 0), 24 | GradientStops = { 25 | [0] = new GradientStop(Color.Yellow, 0.1f), 26 | [1] = new GradientStop(Color.Green, 1.0f), 27 | } 28 | } 29 | }, 30 | ["radial"] = new Frame { 31 | BorderColor = Color.LightGray, 32 | HasShadow = true, 33 | CornerRadius = 12, 34 | HeightRequest = 120, 35 | WidthRequest = 120, 36 | Background = new RadialGradientBrush { 37 | Center = (0.5, 0.5), 38 | Radius = (0.5), 39 | GradientStops = { 40 | [0] = new GradientStop(Color.Red, 0.1f), 41 | [1] = new GradientStop(Color.DarkBlue, 1.0f), 42 | } 43 | } 44 | } 45 | }; 46 | } -------------------------------------------------------------------------------- /src/TabbedPage.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class CurrentPageChangedEventArgs : EventArgs 4 | { 5 | public int Index { get; } 6 | 7 | public CurrentPageChangedEventArgs(int index) => Index = index; 8 | } 9 | 10 | public partial class TabbedPage : Page 11 | { 12 | public TabbedPage() => ElementLists.Add(nameof(Children), tp => (IList) tp.Children); 13 | 14 | public ElementList Children => ElementLists[nameof(Children)]; 15 | 16 | public int CurrentPage { 17 | init => SetValue(nameof(CurrentPage), value, tp => { 18 | tp.CurrentPage = tp.Children[value]; 19 | }); 20 | } 21 | 22 | public Page this[Key key] 23 | { 24 | init => Children[key] = (Element)value; 25 | } 26 | 27 | EventHandler? _handler; 28 | 29 | void OnCurrentPageChanged(object sender, EventArgs args) 30 | { 31 | var page = (Xamarin.Forms.TabbedPage) sender; 32 | var index = page.Children.IndexOf(page.CurrentPage); 33 | if (index != -1) 34 | _handler!(page, new CurrentPageChangedEventArgs(index)); 35 | } 36 | 37 | public Func CurrentPageChanged { 38 | init => SetEvent(nameof(CurrentPageChanged), value, 39 | (ctl, handler) => { 40 | _handler = handler; 41 | ctl.CurrentPageChanged += OnCurrentPageChanged; 42 | }, 43 | (ctl, _) => { 44 | _handler = null; 45 | ctl.CurrentPageChanged -= OnCurrentPageChanged; 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /test/ShapeTests.cs: -------------------------------------------------------------------------------- 1 | using Laconic.Shapes; 2 | 3 | namespace Laconic.Tests; 4 | 5 | public class ShapeTests 6 | { 7 | [Fact] 8 | public void Line_diff() 9 | { 10 | var noDiff = Diff.Calculate(new Line {X1 = 1, Y1 = 2, X2 = 3, Y2 = 4}, 11 | new Line {X1 = 1, Y1 = 2, X2 = 3, Y2 = 4}); 12 | noDiff.Count().ShouldBe(0); 13 | 14 | var diff = Diff.Calculate(new Line {X1 = 100, Y1 = 2, X2 = 3, Y2 = 4}, 15 | new Line {X1 = 1, Y1 = 2, X2 = 3, Y2 = 4}); 16 | var prop = diff.First().ShouldBeOfType(); 17 | prop.Property.ShouldBe(xf.Shapes.Line.X1Property); 18 | prop.Value.ShouldBe(1); 19 | } 20 | 21 | [Fact] 22 | public void Clip_property_is_set() 23 | { 24 | var img = new Image {Clip = new EllipseGeometry {Center = new xf.Point(10, 10), RadiusX = 3, RadiusY = 5}}; 25 | var diff = Diff.Calculate(null, img).ToArray(); 26 | 27 | diff[0].ShouldBeOfType(); 28 | } 29 | 30 | [Fact] 31 | public void Clip_property_not_set_if_value_is_identical() 32 | { 33 | var img = new Image {Clip = new EllipseGeometry {Center = new xf.Point(10, 10), RadiusX = 3, RadiusY = 5}}; 34 | var diff = Diff.Calculate(img, 35 | new Image {Clip = new EllipseGeometry {Center = new xf.Point(10, 10), RadiusX = 3, RadiusY = 5}}); 36 | diff.ShouldBeEmpty(); 37 | } 38 | 39 | [Fact] 40 | public void diffing_Path_as_Clip() 41 | { 42 | var img = new Image {Clip = new PathGeometry("M 10,100 C 100,0 200,200 300,100")}; 43 | var diff = Diff.Calculate(null, img); 44 | 45 | diff.First().ShouldBeOfType(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Behaviors.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public abstract class Behavior : Element 4 | { 5 | public static readonly xf.BindableProperty ValueProperty = xf.BindableProperty.Create( 6 | "Value", 7 | typeof(object), 8 | typeof(xf.Behavior)); 9 | } 10 | 11 | public abstract class Behavior : Behavior where T : Xamarin.Forms.VisualElement 12 | { 13 | protected Behavior() 14 | { 15 | } 16 | 17 | protected Behavior(object? value) => SetValue(ValueProperty, value); 18 | 19 | protected internal override xf.BindableObject CreateView() => new BehaviorAdapter(this); 20 | 21 | protected internal abstract void OnAttachedTo(T bindable); 22 | 23 | protected internal virtual void OnDetachingFrom(T bindable) 24 | { 25 | } 26 | 27 | protected internal virtual void OnValuesUpdated(object value) 28 | { 29 | } 30 | } 31 | 32 | class BehaviorAdapter : xf.Behavior where T : xf.VisualElement 33 | { 34 | readonly Behavior _internal; 35 | 36 | public BehaviorAdapter(Behavior behavior) => _internal = behavior; 37 | 38 | bool _isAttached; 39 | 40 | protected override void OnAttachedTo(T bindable) 41 | { 42 | _internal.OnAttachedTo(bindable); 43 | _isAttached = true; 44 | } 45 | 46 | protected override void OnDetachingFrom(T bindable) 47 | { 48 | _internal.OnDetachingFrom(bindable); 49 | _isAttached = false; 50 | } 51 | 52 | protected override void OnPropertyChanged(string propertyName) 53 | { 54 | if (propertyName == Behavior.ValueProperty.PropertyName && _isAttached) { 55 | _internal.OnValuesUpdated(GetValue(Behavior.ValueProperty)); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /demo/droid/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 35 | 41 | -------------------------------------------------------------------------------- /maps/demo/droid/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 35 | 41 | -------------------------------------------------------------------------------- /test/BasicTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class BasicTests 4 | { 5 | [Fact] 6 | public void visual_element_equality() 7 | { 8 | ((Label?) null == null).ShouldBeTrue(); 9 | 10 | new Label { Text = "a" }.Equals(new Label { Text = "a" }).ShouldBeTrue(); 11 | new Label { Text = "a" }.Equals(new Label { Text = "b" }).ShouldBeFalse(); 12 | 13 | (new Label {Text = "a"} == new Label {Text = "a"}).ShouldBeTrue(); 14 | (new Label {Text = "a"} == new Label {Text = "b"}).ShouldBeFalse(); 15 | 16 | (new Label() == null).ShouldBeFalse(); 17 | } 18 | 19 | [Fact] 20 | public void key_casting_equality() 21 | { 22 | (new Key(1) == 1).ShouldBeTrue(); 23 | (new Key(1L) == 1L).ShouldBeTrue(); 24 | (new Key("a") == "a").ShouldBeTrue(); 25 | 26 | (new Key(2) == 1).ShouldBeFalse(); 27 | (new Key(2L) == 1L).ShouldBeFalse(); 28 | (new Key("a") == "b").ShouldBeFalse(); 29 | } 30 | 31 | [Fact(Skip="TODO: removed when refactoring LocalContext")] 32 | public void throw_on_setting_child_key_twice() => 33 | Should.Throw(() => 34 | { 35 | var _ = new StackLayout {["1"] = new Label(), ["1"] = new Label()}; 36 | }).Message.ShouldBe("An item with the same key has already been added. Key: 1"); 37 | 38 | [Fact] 39 | public void Signal_deconstruction() 40 | { 41 | var g1 = new Signal("a", "b"); 42 | var (a, b) = g1; 43 | a.ShouldBe("a"); 44 | b.ShouldBe("b"); 45 | 46 | // TODO: Does it even make sense? 47 | var g2 = new Signal("c"); 48 | var (c, d) = g2; 49 | c.ShouldBe("c"); 50 | d.ShouldBeNull(); 51 | } 52 | } -------------------------------------------------------------------------------- /src/ImageSource.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace Laconic; 5 | 6 | public abstract class ImageSource : Element 7 | { 8 | public static ImageSource FromFile(string file) => new FileImageSource {File = file}; 9 | 10 | public static ImageSource FromResource(string resource, Type resolvingType) => 11 | FromResource(resource, resolvingType.GetTypeInfo().Assembly); 12 | 13 | public static ImageSource FromResource(string resource, Assembly? sourceAssembly = null) => 14 | throw new NotImplementedException(); 15 | 16 | public static ImageSource FromStream(Func stream) => throw new NotImplementedException(); 17 | 18 | public static ImageSource FromUri(Uri uri) => new UriImageSource {Uri = uri}; 19 | 20 | public static implicit operator ImageSource?(string? source) 21 | { 22 | // Taken from xf.ImageSourceConverter.ConvertFromInvariantString(source) 23 | if (source == null) 24 | return null; 25 | 26 | return Uri.TryCreate(source, UriKind.Absolute, out var uri) && uri.Scheme != "file" 27 | ? FromUri(uri) 28 | : FromFile(source); 29 | } 30 | } 31 | 32 | public partial class FontImageSource : ImageSource 33 | { 34 | protected internal override xf.BindableObject CreateView() => new xf.FontImageSource(); 35 | } 36 | 37 | public partial class FileImageSource : ImageSource 38 | { 39 | protected internal override xf.BindableObject CreateView() => new xf.FileImageSource(); 40 | } 41 | 42 | public class UriImageSource : ImageSource 43 | { 44 | public Uri Uri { 45 | get => GetValue(xf.UriImageSource.UriProperty); 46 | set => SetValue(xf.UriImageSource.UriProperty, value); 47 | } 48 | 49 | protected internal override xf.BindableObject CreateView() => new xf.UriImageSource(); 50 | } -------------------------------------------------------------------------------- /src/GridViewList.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class GridViewList : ViewList 4 | { 5 | internal (int Row, int Column, int RowSpan, int ColumnSpan) GetPositioning(Key key) => _positioning[key]; 6 | 7 | internal void SetPositioning(Key key, int row, int column, int rowSpan, int columnSpan) => 8 | _positioning[key] = (row, column, rowSpan, columnSpan); 9 | 10 | readonly Dictionary _positioning = new(); 11 | 12 | public void Add(Key key, View? blueprint, int row = 0, int column = 0, int rowSpan = 1, int columnSpan = 1) 13 | { 14 | if (blueprint != null) { 15 | base.Add(key, blueprint); 16 | SetPositioning(key, row, column, rowSpan, columnSpan); 17 | } 18 | } 19 | 20 | public static implicit operator GridViewList(Dictionary<(Key Key, int Row, int Column), View?> source) 21 | { 22 | var res = new GridViewList(); 23 | foreach (var item in source.Where(x => x.Value != null)) 24 | { 25 | res.Add(item.Key.Key, item.Value); 26 | res.SetPositioning(item.Key.Key, item.Key.Row, item.Key.Column, 1, 1); 27 | } 28 | 29 | return res; 30 | } 31 | } 32 | 33 | public class ItemsViewList : ViewList 34 | { 35 | internal readonly Dictionary ReuseKeys = new(); 36 | 37 | public View? this[string reuseKey, Key key] 38 | { 39 | set 40 | { 41 | if (value != null) { 42 | base[key] = value; 43 | ReuseKeys[key] = reuseKey; 44 | } 45 | } 46 | } 47 | 48 | public void Add(string reuseKey, Key key, View? view) 49 | { 50 | if (view != null) { 51 | Add(key, view); 52 | ReuseKeys[key] = reuseKey; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /demo/app/BehaviorPage.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Xamarin.Forms; 3 | 4 | namespace Laconic.Demo; 5 | 6 | class AnimateTextSize : Behavior 7 | { 8 | protected override void OnAttachedTo(xf.Label bindable) => bindable.PropertyChanged += OnPropertyChanged; 9 | 10 | protected override void OnDetachingFrom(xf.Label bindable) => bindable.PropertyChanged -= OnPropertyChanged; 11 | 12 | void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 13 | { 14 | if (e.PropertyName == xf.Label.TextProperty.PropertyName) { 15 | var lbl = (xf.Label)sender; 16 | lbl.Animate("text-size", s => lbl.Scale = s, 5.0, 1.0); 17 | } 18 | } 19 | } 20 | 21 | static class BehaviorPage 22 | { 23 | public static View Content() => Element.WithContext("behavior", ctx => { 24 | var (count, setCount) = ctx.UseLocalState(0); 25 | return new Grid { 26 | ["lbl"] = new Label { 27 | Text = count.ToString(), 28 | FontSize = 50, 29 | Behaviors = {["anim"] = new AnimateTextSize()}, 30 | HorizontalOptions = LayoutOptions.Center, 31 | VerticalOptions = LayoutOptions.Center, 32 | }, ["btn", row: 1] = new Button { 33 | Text = "Update", 34 | Clicked = () => setCount(count + 1), 35 | TextColor = Color.White, 36 | FontSize = 20, 37 | BackgroundColor = Color.Coral, 38 | BorderColor = Color.Chocolate, 39 | BorderWidth = 2, 40 | CornerRadius = 20, 41 | Margin = (0, 30), 42 | Padding = (30, 0), 43 | HorizontalOptions = LayoutOptions.Center, 44 | VerticalOptions = LayoutOptions.End, 45 | } 46 | }; 47 | }); 48 | } -------------------------------------------------------------------------------- /maps/demo/app/SignalsStateReducers.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Laconic.Maps.Demo 4 | { 5 | enum FeatureSwitch 6 | { 7 | IsShowingUser, 8 | TrafficEnabled, 9 | HasScrollEnabled, 10 | HasZoomEnabled 11 | } 12 | 13 | record City(string Name, int Population, Polygon[] Boundaries, bool IsShownOnMap); 14 | record Features(bool IsShowingUser, bool TrafficEnabled, bool HasScrollEnabled, bool HasZoomEnabled); 15 | record State(MapType MapType, Features Features, City[] Cities); 16 | 17 | static class Reducers 18 | { 19 | static Features Features(Features features, Signal signal) => signal switch { 20 | (FeatureSwitch.IsShowingUser, bool val) => features with { IsShowingUser = val }, 21 | (FeatureSwitch.TrafficEnabled, bool val) => features with { TrafficEnabled = val }, 22 | (FeatureSwitch.HasScrollEnabled, bool val) => features with { HasScrollEnabled = val }, 23 | (FeatureSwitch.HasZoomEnabled, bool val) => features with { HasZoomEnabled = val }, 24 | _ => features 25 | }; 26 | 27 | static City[] Cities(City[] cities, Signal signal) => signal switch { 28 | ("toggle-city", string cityName) => cities 29 | .Select(c => c.Name == cityName ? c with { IsShownOnMap = ! c.IsShownOnMap } : c).ToArray(), 30 | _ => cities 31 | }; 32 | 33 | public static State Main(State state, Signal signal) => signal switch { 34 | (MapType t, _) => state with { MapType = t }, 35 | _ => state with { Features = Features(state.Features, signal), Cities = Cities(state.Cities, signal) } 36 | }; 37 | 38 | } 39 | } 40 | 41 | // Records won't work without this 42 | namespace System.Runtime.CompilerServices 43 | { 44 | sealed class IsExternalInit 45 | { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/app/SwipeViewPage.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | static class SwipeViewPage 4 | { 5 | public static VisualElement Content() => Element.WithContext(ctx => { 6 | var (text, setText) = ctx.UseLocalState(""); 7 | 8 | return new StackLayout { 9 | [0] = new SwipeView { 10 | HeightRequest = 50, 11 | VerticalOptions = LayoutOptions.CenterAndExpand, 12 | LeftItems = { 13 | ["fav"] = new SwipeItem { 14 | Text = "Favourite", 15 | IconImageSource = new FontImageSource {Size = 15, FontFamily = "IconFont", Glyph = "\uf02e"}, 16 | BackgroundColor = Color.LightGreen, 17 | Invoked = _ => setText("Favourite") 18 | }, 19 | ["del"] = new SwipeItem { 20 | Text = "Delete", 21 | IconImageSource = new FontImageSource {Size = 15, FontFamily = "IconFont", Glyph = "\uf2ed"}, 22 | BackgroundColor = Color.LightPink, 23 | Invoked = _ => setText("Delete") 24 | } 25 | }, 26 | Content = new Grid { 27 | HeightRequest = 60, 28 | WidthRequest = 300, 29 | [0] = new Label { 30 | Text = "Swipe right", 31 | HorizontalOptions = LayoutOptions.Center, 32 | VerticalOptions = LayoutOptions.Center 33 | } 34 | } 35 | }, 36 | ["lbl"] = text == "" ? null : new Label { 37 | Text = $"Invoked: {text}", 38 | HorizontalOptions = LayoutOptions.Center, 39 | Margin = (0, 50) 40 | } 41 | }; 42 | }); 43 | } -------------------------------------------------------------------------------- /src/Key.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class Key : IEquatable 4 | { 5 | readonly object _value; 6 | 7 | // TODO: overloads for allowed types 8 | public Key(object value) => _value = value; 9 | 10 | public bool Equals(Key other) => _value.Equals(other._value); 11 | 12 | public override bool Equals(object other) => other is Key key && this.Equals(key); 13 | 14 | public override int GetHashCode() => _value.GetHashCode(); 15 | 16 | public override string ToString() => _value.ToString(); 17 | 18 | public static bool operator ==(Key lhs, Key rhs) => lhs._value.Equals(rhs._value); 19 | public static bool operator !=(Key lhs, Key rhs) => !lhs._value.Equals(rhs._value); 20 | 21 | // Must provide implicit conversions for all primitive types allowed as keys (string, int, long, guid) 22 | // later: DateTime, DateTimeOffset 23 | 24 | public static bool operator ==(Key lhs, string rhs) => lhs._value is string && lhs._value.Equals(rhs); 25 | public static bool operator !=(Key lhs, string rhs) => !(lhs._value is string && lhs._value.Equals(rhs)); 26 | public static implicit operator Key(string value) => new(value); 27 | 28 | public static bool operator ==(Key lhs, int rhs) => lhs._value is int && lhs._value.Equals(rhs); 29 | public static bool operator !=(Key lhs, int rhs) => !(lhs._value is int && lhs._value.Equals(rhs)); 30 | public static implicit operator Key(int value) => new(value); 31 | 32 | public static bool operator ==(Key lhs, long rhs) => lhs._value.Equals(rhs); 33 | public static bool operator !=(Key lhs, long rhs) => !lhs._value.Equals(rhs); 34 | public static implicit operator Key(long value) => new(value); 35 | 36 | public static bool operator ==(Key lhs, Guid rhs) => lhs._value.Equals(rhs); 37 | public static bool operator !=(Key lhs, Guid rhs) => !lhs._value.Equals(rhs); 38 | public static implicit operator Key(Guid value) => new(value); 39 | } -------------------------------------------------------------------------------- /demo/app/DynamicGrid.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Demo; 2 | 3 | class GridSignal : Signal 4 | { 5 | public GridSignal(string type, double value) : base(type, value) 6 | { 7 | } 8 | } 9 | 10 | static class DynamicGrid 11 | { 12 | public static (int Rows, int Columns) Reducer((int Rows, int Columns) state, GridSignal signal) => signal switch 13 | { 14 | ("r", double val) => ((int)Math.Round(val), state.Columns), 15 | ("c", double val) => (state.Rows, (int)Math.Round(val)), 16 | _ => throw new NotImplementedException() 17 | }; 18 | 19 | public static StackLayout Content((int Rows, int Columns) state) => new() { 20 | BackgroundColor = Color.Bisque, 21 | Padding = 50, 22 | ["rowsLabel"] = new Label {Text = "Rows:"}, 23 | ["rowsSlider"] = 24 | new Slider {Maximum = 10, Minimum = 2, Value = state.Rows, ValueChanged = e => new GridSignal("r", e.NewValue)}, 25 | ["colsLabel"] = new Label {Text = "Columns:"}, 26 | ["colsSlider"] = new Slider 27 | { 28 | Maximum = 6, Minimum = 2, Value = state.Columns, ValueChanged = e => new GridSignal("c", e.NewValue) 29 | }, 30 | ["grid"] = new Grid 31 | { 32 | Children = (from r in Enumerable.Range(0, state.Rows) 33 | from c in Enumerable.Range(0, state.Columns) 34 | select (Row: r, Column: c)) 35 | .ToGridViewList(x => (x.Row * state.Columns + x.Column, x.Row, x.Column), 36 | x => new Label 37 | { 38 | Text = $"R{x.Row}C{x.Column}", 39 | VerticalTextAlignment = TextAlignment.Center, 40 | HorizontalTextAlignment = TextAlignment.Center, 41 | HeightRequest = 70, 42 | BackgroundColor = Color.Chocolate, 43 | TextColor = Color.White 44 | }) 45 | }, 46 | }; 47 | } -------------------------------------------------------------------------------- /codegen/test/SignalTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using Shouldly; 3 | 4 | namespace Laconic.CodeGeneration.Tests 5 | { 6 | public class Signal 7 | { 8 | public readonly object? Payload; 9 | readonly object? _p1; 10 | readonly object? _p2; 11 | 12 | public Signal(object? payload) => (Payload, _p1, _p2) = (payload, payload, null); 13 | public Signal(object p1, object? p2) => (Payload, _p1, _p2) = (p1, p1, p2); 14 | 15 | public void Deconstruct(out object? p1, out object? p2) => (p1, p2) = (_p1, _p2); 16 | 17 | public override string ToString() => $"{GetType()}: {_p1?.ToString()} {_p2?.ToString()}"; 18 | } 19 | 20 | public class Signal : Signal 21 | { 22 | public Signal(T payload) : base(payload) 23 | { 24 | } 25 | 26 | public new T Payload => (T) base.Payload!; 27 | } 28 | 29 | [Signals] 30 | interface __MySignal 31 | { 32 | Signal NoPayload(); 33 | Signal WithOneParam(string id); 34 | Signal WithTwoParams(int id, string name); 35 | Signal WithThreeParams(int id, string first, string second); 36 | } 37 | 38 | public class SignalTests 39 | { 40 | [Fact] 41 | public void Signal_generation_works() 42 | { 43 | var noPayload = new NoPayload(); 44 | 45 | noPayload.ShouldBeAssignableTo(); 46 | noPayload.ShouldBeAssignableTo(); 47 | noPayload.Payload.ShouldBeNull(); 48 | 49 | var twoParams = new WithTwoParams(1, "one"); 50 | twoParams.Id.ShouldBe(1); 51 | twoParams.Name.ShouldBe("one"); 52 | var (id, name) = twoParams; 53 | id.ShouldBe(1); 54 | name.ShouldBe("one"); 55 | 56 | var threeParams = new WithThreeParams(1, "one", "two"); 57 | 58 | threeParams.Id.ShouldBe(1); 59 | threeParams.First.ShouldBe("one"); 60 | threeParams.Second.ShouldBe("two"); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/Pages.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | // TODO: MenuItem etc. 4 | public class ToolbarItem : Element 5 | { 6 | public ImageSource IconImageSource { 7 | init => SetValue(xf.MenuItem.IconImageSourceProperty, value); 8 | } 9 | 10 | public string Text { 11 | init => SetValue(xf.MenuItem.TextProperty, value); 12 | } 13 | 14 | public Func Clicked 15 | { 16 | init => SetEvent(nameof(Clicked), value, 17 | (ctl, handler) => ctl.Clicked += handler, 18 | (ctl, handler) => ctl.Clicked -= handler); 19 | } 20 | 21 | protected internal override xf.BindableObject CreateView() => new xf.ToolbarItem(); 22 | } 23 | 24 | public interface Page 25 | { 26 | 27 | } 28 | 29 | public abstract partial class Page : VisualElement, Page where T : Xamarin.Forms.Page, new() 30 | { 31 | public IDictionary ToolbarItems { get; } = new Dictionary(); 32 | } 33 | 34 | public class ContentPage : Page, IContentHost 35 | { 36 | public View? Content { get; set; } 37 | 38 | public string BackButtonTitle { 39 | init => ProvidedValues[xf.NavigationPage.BackButtonTitleProperty] = value; 40 | } 41 | 42 | // TODO: 43 | // HasNavigationBarProperty 44 | //HasBackButtonProperty 45 | // TitleIconImageSourceProperty 46 | // IconColorProperty 47 | public View TitleView { 48 | init => ProvidedValues[xf.NavigationPage.TitleViewProperty] = value; 49 | } 50 | 51 | public override string ToString() => "ContentPage{" + Content + "}"; 52 | } 53 | 54 | public partial class FlyoutPage : Page 55 | { 56 | public Element? Flyout { set; get; } 57 | public Element? Detail { set; get; } 58 | 59 | public Func BackButtonPressed { 60 | init => SetEvent(nameof(BackButtonPressed), value, 61 | (ctl, handler) => ctl.BackButtonPressed += handler, 62 | (ctl, handler) => ctl.BackButtonPressed -= handler); 63 | } 64 | } -------------------------------------------------------------------------------- /codegen/README.md: -------------------------------------------------------------------------------- 1 | # Code generation of Records and Unions for C# 8 2 | 3 | Install the package from NuGet: [Laconic.CodeGeneration](https://www.nuget.org/packages/Laconic.CodeGeneration/0.9.3-beta). This is compile-time only dependency. 4 | 5 | ## Records 6 | 7 | Write an interface marked with `RecordsAttribute`, the interface name doesn't matter: 8 | 9 | ```csharp 10 | [Records] 11 | public interface MyRecords 12 | { 13 | record User(string firstName, string lastName); 14 | } 15 | ``` 16 | 17 | **What's generated:** each method in the interface becomes an immutable partial class, with a constructor, value equality and `With` method. Each parameter in the method becomes a property: 18 | 19 | ```csharp 20 | var johnny = new User("Johnny", "Smith"); 21 | Console.WriteLine(johnny.FirstName); // prints "Johnny" 22 | Console.WriteLine(johhny == new User("Johnny", "Smith")); // prints "true" 23 | 24 | var grownUp = johnny.With(firstName: "John"); // values that are not supplied are copied from the original object 25 | Console.WriteLine(johnny == grownUp); // prints "false" 26 | ``` 27 | 28 | ## Unions 29 | 30 | Write an interface marked with `UnionAttribute` using one or more underscores as suffix or prefix. 31 | 32 | ```csharp 33 | [Union] 34 | interface __Shape__ 35 | { 36 | record Circle(double radius); 37 | record Rectangle(double length, double width); 38 | } 39 | ``` 40 | 41 | **What's generated:** The name of the interface with stripped underscores becomes the name of your union, each method becomes a record implementing the union interface: 42 | 43 | ```csharp 44 | var circle = new Circle(10); 45 | var rect = new Rectangle(10, 20); 46 | 47 | double Area(Shape shape) => shape switch { 48 | Circle c => Math.PI * c.Radius * c.Radius, 49 | Rectangle r => r.Length * r.Height, 50 | _ => throw new NotImplementedException() 51 | }; 52 | ``` 53 | 54 | ## Credits 55 | 56 | The inspiration (and some code) came from the excellent [LanguageExt](https://github.com/louthy/language-ext) project. Check it out! 57 | 58 | All the heavy lifting is done by [CodeGeneration.Roslyn](https://github.com/AArnott/CodeGeneration.Roslyn). 59 | -------------------------------------------------------------------------------- /src/GridDiff.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | enum GridPositionChangeType 4 | { 5 | Row, 6 | Column, 7 | RowSpan, 8 | ColumnSpan 9 | } 10 | 11 | static class GridDiff 12 | { 13 | public static DiffOperation? CalculateRowDefinitionsDiff( 14 | Grid? existingGrid, Grid newGrid) => (existingGrid, newGrid) switch { 15 | (_, null) => null, 16 | (null, _) => new RowDefinitionsChange(newGrid.RowDefinitions.ToArray()), 17 | var (e, n) when n.RowDefinitions.Equals(e.RowDefinitions) => null, 18 | (_, _) => new RowDefinitionsChange(newGrid.RowDefinitions.ToArray()) 19 | }; 20 | 21 | public static DiffOperation? CalculateColumnDefinitionsDiff( 22 | Grid? existingGrid, Grid newGrid) => (existingGrid, newGrid) switch { 23 | (_, null) => null, 24 | (null, _) => new ColumnDefinitionsChange(newGrid.ColumnDefinitions.ToArray()), 25 | var (e, n) when n.ColumnDefinitions.Equals(e.ColumnDefinitions) => null, 26 | (_, _) => new ColumnDefinitionsChange(newGrid.ColumnDefinitions.ToArray()) 27 | }; 28 | 29 | public static IEnumerable CalculatePositioningInGrid(Key key, GridViewList? existingList, 30 | GridViewList newList) 31 | { 32 | var existingPos = (Row: 0, Column: 0, RowSpan: 1, ColumnSpan: 1); 33 | if (existingList != null && existingList.ContainsKey(key)) 34 | existingPos = existingList.GetPositioning(key); 35 | 36 | var newPos = newList.GetPositioning(key); 37 | 38 | if (newPos.Row != existingPos.Row) 39 | yield return new GridPositionChange(GridPositionChangeType.Row, newPos.Row); 40 | 41 | if (newPos.Column != existingPos.Column) 42 | yield return new GridPositionChange(GridPositionChangeType.Column, newPos.Column); 43 | 44 | if (newPos.RowSpan != existingPos.RowSpan) 45 | yield return new GridPositionChange(GridPositionChangeType.RowSpan, newPos.RowSpan); 46 | 47 | if (newPos.ColumnSpan != existingPos.ColumnSpan) 48 | yield return new GridPositionChange(GridPositionChangeType.ColumnSpan, newPos.ColumnSpan); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Size.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public struct Size 4 | { 5 | readonly double _width; 6 | 7 | readonly double _height; 8 | 9 | public static readonly Size Zero = new(); 10 | 11 | public bool IsZero { 12 | get { 13 | if (_width == 0.0) { 14 | return _height == 0.0; 15 | } 16 | 17 | return false; 18 | } 19 | } 20 | 21 | public double Width => _width; 22 | 23 | public double Height => _height; 24 | 25 | public Size(double width, double height) 26 | { 27 | if (double.IsNaN(width)) { 28 | throw new ArgumentException("NaN is not a valid value for width"); 29 | } 30 | 31 | if (double.IsNaN(height)) { 32 | throw new ArgumentException("NaN is not a valid value for height"); 33 | } 34 | 35 | _width = width; 36 | _height = height; 37 | } 38 | 39 | public static Size operator +(Size s1, Size s2) => new(s1._width + s2._width, s1._height + s2._height); 40 | 41 | public static Size operator -(Size s1, Size s2) => new(s1._width - s2._width, s1._height - s2._height); 42 | 43 | public static Size operator *(Size s1, double value) => new(s1._width * value, s1._height * value); 44 | 45 | public static bool operator ==(Size s1, Size s2) 46 | { 47 | if (s1._width == s2._width) { 48 | return s1._height == s2._height; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | public static bool operator !=(Size s1, Size s2) 55 | { 56 | if (s1._width == s2._width) { 57 | return s1._height != s2._height; 58 | } 59 | 60 | return true; 61 | } 62 | 63 | public static explicit operator Point(Size size) => new(size.Width, size.Height); 64 | 65 | bool Equals(Size other) => _width.Equals(other._width) && _height.Equals(other._height); 66 | 67 | public override bool Equals(object obj) => obj switch { 68 | null => false, 69 | Size size => Equals(size), 70 | _ => false 71 | }; 72 | 73 | public override int GetHashCode() => (_width.GetHashCode() * 397) ^ _height.GetHashCode(); 74 | } -------------------------------------------------------------------------------- /test/MiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class MiddlewareTests 4 | { 5 | [Fact] 6 | public void middleware_is_called() 7 | { 8 | var binder = Binder.Create("", (s, g) => s); 9 | var isCalled = false; 10 | binder.UseMiddleware((context, next) => 11 | { 12 | isCalled = true; 13 | return next(context); 14 | }); 15 | 16 | binder.ProcessSignal(new Signal("_")); 17 | 18 | isCalled.ShouldBeTrue(); 19 | } 20 | 21 | [Fact] 22 | public void middleware_modifies_state_before_reducer() 23 | { 24 | var binder = Binder.Create("initial", (s, g) => s + " - reducer"); 25 | binder.UseMiddleware((context, next) => 26 | { 27 | var modified = context.WithState(context.State + " - modified"); 28 | return next(modified); 29 | }); 30 | 31 | binder.ProcessSignal(new Signal("_")); 32 | 33 | binder.State.ShouldBe("initial - modified - reducer"); 34 | } 35 | 36 | [Fact] 37 | public void middleware_modifies_state_after_reducer() 38 | { 39 | var binder = Binder.Create("initial", (s, g) => "reducer"); 40 | binder.UseMiddleware((context, next) => 41 | { 42 | var ctx = next(context); 43 | return ctx.WithState(ctx.State + " - modified"); 44 | }); 45 | 46 | binder.ProcessSignal(new Signal("_")); 47 | 48 | binder.State.ShouldBe("reducer - modified"); 49 | } 50 | 51 | [Fact] 52 | public void middleware_can_be_chained() 53 | { 54 | var binder = Binder.Create("initial", (s, g) => "reducer"); 55 | binder.UseMiddleware((context, next) => 56 | { 57 | var ctx = next(context); 58 | return ctx.WithState(ctx.State + " - outer"); 59 | }); 60 | binder.UseMiddleware((context, next) => 61 | { 62 | var ctx = next(context); 63 | return ctx.WithState(ctx.State + " - inner"); 64 | }); 65 | 66 | binder.ProcessSignal(new Signal("_")); 67 | 68 | binder.State.ShouldBe("reducer - inner - outer"); 69 | } 70 | } -------------------------------------------------------------------------------- /codegen/CodeGen.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGen.Tests", "test\CodeGen.Tests.csproj", "{1CCADF8C-19AB-4D3C-B049-9ED8D730405B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGen.Attributes", "src\attributes\CodeGen.Attributes.csproj", "{35932F8A-46DA-4E46-890F-61FED0421CC3}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGen.Generators", "src\generators\CodeGen.Generators.csproj", "{865FE8BA-00B4-46DB-BC82-B5FA0D985A5F}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {1CCADF8C-19AB-4D3C-B049-9ED8D730405B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {1CCADF8C-19AB-4D3C-B049-9ED8D730405B}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {1CCADF8C-19AB-4D3C-B049-9ED8D730405B}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {1CCADF8C-19AB-4D3C-B049-9ED8D730405B}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {35932F8A-46DA-4E46-890F-61FED0421CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {35932F8A-46DA-4E46-890F-61FED0421CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {35932F8A-46DA-4E46-890F-61FED0421CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {35932F8A-46DA-4E46-890F-61FED0421CC3}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {865FE8BA-00B4-46DB-BC82-B5FA0D985A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {865FE8BA-00B4-46DB-BC82-B5FA0D985A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {865FE8BA-00B4-46DB-BC82-B5FA0D985A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {865FE8BA-00B4-46DB-BC82-B5FA0D985A5F}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {18247316-A7C2-4598-82BF-F9103F2C6084} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/Thickness.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public readonly struct Thickness 4 | { 5 | public static implicit operator Thickness(double uniformSize) => new(uniformSize); 6 | 7 | public static implicit operator Thickness((double horizontalSize, double verticalSize) values) 8 | => new(values.horizontalSize, values.verticalSize); 9 | 10 | public static implicit operator Thickness((double left, double top, double right, double bottom) values) 11 | => new(values.left, values.top, values.right, values.bottom); 12 | 13 | public double Left { get; } 14 | public double Top { get; } 15 | public double Right { get; } 16 | public double Bottom { get; } 17 | 18 | Thickness(double uniformSize) => this = new Thickness(uniformSize, uniformSize, uniformSize, uniformSize); 19 | 20 | Thickness(double horizontalSize, double verticalSize) => 21 | this = new Thickness(horizontalSize, verticalSize, horizontalSize, verticalSize); 22 | 23 | Thickness(double left, double top, double right, double bottom) 24 | { 25 | this = default(Thickness); 26 | Left = left; 27 | Top = top; 28 | Right = right; 29 | Bottom = bottom; 30 | } 31 | 32 | bool Equals(Thickness other) 33 | { 34 | if (Left.Equals(other.Left) && Top.Equals(other.Top) && Right.Equals(other.Right)) { 35 | return Bottom.Equals(other.Bottom); 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public override bool Equals(object obj) => obj switch { 42 | null => false, 43 | Thickness thickness => Equals(thickness), 44 | _ => false 45 | }; 46 | 47 | public override int GetHashCode() => 48 | (((((Left.GetHashCode() * 397) ^ Top.GetHashCode()) * 397) ^ Right.GetHashCode()) * 397) ^ 49 | Bottom.GetHashCode(); 50 | 51 | public static bool operator ==(Thickness left, Thickness right) => left.Equals(right); 52 | 53 | public static bool operator !=(Thickness left, Thickness right) => !left.Equals(right); 54 | 55 | public void Deconstruct(out double left, out double top, out double right, out double bottom) 56 | { 57 | left = Left; 58 | top = Top; 59 | right = Right; 60 | bottom = Bottom; 61 | } 62 | } -------------------------------------------------------------------------------- /src/Point.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Laconic; 4 | 5 | public readonly struct Point : IConvert 6 | { 7 | public static Point Zero; 8 | 9 | public double X { get; } 10 | 11 | public double Y { get; } 12 | 13 | public bool IsEmpty { 14 | get { 15 | if (X == 0.0) { 16 | return Y == 0.0; 17 | } 18 | 19 | return false; 20 | } 21 | } 22 | 23 | public override string ToString() => 24 | $"{{X={X.ToString(CultureInfo.InvariantCulture)} Y={Y.ToString(CultureInfo.InvariantCulture)}}}"; 25 | 26 | public object ToNative() => new Xamarin.Forms.Point(X, Y); 27 | 28 | public Point(double x, double y) 29 | { 30 | this = default(Point); 31 | X = x; 32 | Y = y; 33 | } 34 | 35 | public Point(Size sz) 36 | { 37 | this = default(Point); 38 | X = sz.Width; 39 | Y = sz.Height; 40 | } 41 | 42 | public override bool Equals(object o) 43 | { 44 | if (!(o is Point)) { 45 | return false; 46 | } 47 | 48 | return this == (Point) o; 49 | } 50 | 51 | public override int GetHashCode() => X.GetHashCode() ^ (Y.GetHashCode() * 397); 52 | 53 | public static explicit operator Size(Point pt) => new(pt.X, pt.Y); 54 | 55 | public static Point operator +(Point pt, Size sz) => new(pt.X + sz.Width, pt.Y + sz.Height); 56 | 57 | public static Point operator -(Point pt, Size sz) => new(pt.X - sz.Width, pt.Y - sz.Height); 58 | 59 | public static bool operator ==(Point ptA, Point ptB) 60 | { 61 | if (ptA.X == ptB.X) { 62 | return ptA.Y == ptB.Y; 63 | } 64 | 65 | return false; 66 | } 67 | 68 | public static bool operator !=(Point ptA, Point ptB) 69 | { 70 | if (ptA.X == ptB.X) { 71 | return ptA.Y != ptB.Y; 72 | } 73 | 74 | return true; 75 | } 76 | 77 | public static implicit operator Point((double X, double Y) value) => new(value.X, value.Y); 78 | 79 | public double Distance(Point other) => Math.Sqrt(Math.Pow(X - other.X, 2.0) + Math.Pow(Y - other.Y, 2.0)); 80 | 81 | public void Deconstruct(out double x, out double y) 82 | { 83 | x = X; 84 | y = Y; 85 | } 86 | } -------------------------------------------------------------------------------- /binding-report.md: -------------------------------------------------------------------------------- 1 | ## Not Used 2 | 3 | AdaptiveTrigger 4 | 5 | Application 6 | 7 | Cell 8 | 9 | CompareStateTrigger 10 | 11 | ContentPresenter 12 | 13 | DataTrigger 14 | 15 | DeviceStateTrigger 16 | 17 | EntryCell 18 | 19 | EventTrigger 20 | 21 | FlexLayout 22 | 23 | GestureElement 24 | 25 | GroupableItemsView 26 | 27 | HtmlWebViewSource 28 | 29 | ImageCell 30 | 31 | MasterDetailPage 32 | 33 | MultiPage`1 34 | 35 | MultiTrigger 36 | 37 | NavigableElement 38 | 39 | OrientationStateTrigger 40 | 41 | SearchHandler 42 | 43 | StateTrigger 44 | 45 | StateTriggerBase 46 | 47 | SwitchCell 48 | 49 | TableSectionBase 50 | 51 | TableSectionBase`1 52 | 53 | TableView 54 | 55 | TemplatedItemsList`2 56 | 57 | TemplatedPage 58 | 59 | TemplatedView 60 | 61 | TextCell 62 | 63 | Trigger 64 | 65 | TriggerBase 66 | 67 | UrlWebViewSource 68 | 69 | ViewCell 70 | 71 | WebViewSource 72 | 73 | ## Not Implemented 74 | 75 | AppLinkEntry 76 | 77 | ArcSegment 78 | 79 | BackButtonBehavior 80 | 81 | BaseMenuItem 82 | 83 | BaseShellItem 84 | 85 | Behavior 86 | 87 | Behavior`1 88 | 89 | BezierSegment 90 | 91 | ClickGestureRecognizer 92 | 93 | CompositeTransform 94 | 95 | FlyoutItem 96 | 97 | GeometryGroup 98 | 99 | GridItemsLayout 100 | 101 | ItemsLayout 102 | 103 | LinearItemsLayout 104 | 105 | LineSegment 106 | 107 | MatrixTransform 108 | 109 | Menu 110 | 111 | MenuItem 112 | 113 | OpenGLView 114 | 115 | PanGestureRecognizer 116 | 117 | PathGeometry 118 | 119 | PathSegment 120 | 121 | PinchGestureRecognizer 122 | 123 | PolyBezierSegment 124 | 125 | PolyLineSegment 126 | 127 | PolyQuadraticBezierSegment 128 | 129 | QuadraticBezierSegment 130 | 131 | RectangleGeometry 132 | 133 | RelativeLayout 134 | 135 | RotateTransform 136 | 137 | ScaleTransform 138 | 139 | Shell 140 | 141 | ShellContent 142 | 143 | ShellGroupItem 144 | 145 | ShellItem 146 | 147 | ShellSection 148 | 149 | SkewTransform 150 | 151 | StreamImageSource 152 | 153 | SwipeGestureRecognizer 154 | 155 | SwipeItemView 156 | 157 | Tab 158 | 159 | TabBar 160 | 161 | Transform 162 | 163 | TransformGroup 164 | 165 | TranslateTransform 166 | 167 | ## Undefined 168 | 169 | DragGestureRecognizer 170 | 171 | DropGestureRecognizer 172 | 173 | RoundRectangleGeometry 174 | 175 | -------------------------------------------------------------------------------- /maps/demo/app/CityLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Reflection; 4 | using GeoJSON.Net.Feature; 5 | using Newtonsoft.Json; 6 | 7 | namespace Laconic.Maps.Demo 8 | { 9 | class CityLoader 10 | { 11 | public static City[] Load() 12 | { 13 | var assembly = typeof(City).GetTypeInfo().Assembly; 14 | // data source: https://github.com/drei01/geojson-world-cities 15 | var stream = assembly.GetManifestResourceStream(assembly.GetName().Name + ".largest-cities.geojson"); 16 | var json = ""; 17 | using (var reader = new StreamReader (stream)) 18 | { 19 | json = reader.ReadToEnd (); 20 | } 21 | 22 | var source = JsonConvert.DeserializeObject (json); 23 | 24 | static Polygon[] GetPolygons(FeatureCollection col, string cityName) 25 | { 26 | // Query 27 | 28 | var sourcePolygons = 29 | from feat in col.Features 30 | from prop in feat.Properties 31 | from poly in (feat.Geometry as GeoJSON.Net.Geometry.Polygon).Coordinates 32 | where prop.Value.Equals(cityName.ToUpper()) 33 | select poly.Coordinates; 34 | 35 | // Transform 36 | 37 | return sourcePolygons.Select( 38 | p => new Polygon { 39 | FillColor = (0, 0, 255, 25), 40 | StrokeColor = Color.Blue, 41 | StrokeWidth = 1, 42 | Geopath = p.Select(x => new Position(x.Latitude, x.Longitude)).ToList() 43 | }).ToArray(); 44 | } 45 | 46 | return new[] { 47 | new City("Tokyo", 37_400_068, GetPolygons(source, "Tokyo"), false), 48 | new City("Delhi", 28_514_000, GetPolygons(source, "New Delhi"), false), // can't find boundaries for Delhi 49 | new City("Shanghai", 25_582_000, GetPolygons(source, "Shanghai"), false), 50 | new City("São Paulo", 21_650_000, GetPolygons(source, "Sao Paulo"), false), 51 | new City("Mexico City", 21_650_000, GetPolygons(source, "Mexico City"), false) 52 | }; 53 | } 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /maps/src/Map.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using xf = Xamarin.Forms; 3 | 4 | namespace Laconic.Maps 5 | { 6 | public enum MapType 7 | { 8 | Street, 9 | Satellite, 10 | Hybrid 11 | } 12 | 13 | public enum PinType 14 | { 15 | Generic, 16 | Place, 17 | SavedPin, 18 | SearchResult 19 | } 20 | 21 | public class Map : View 22 | { 23 | public Map() 24 | { 25 | // TODO: generic parameter or casting should not be necessary 26 | ElementLists.Add(nameof(Pins), map => (IList) map.Pins); 27 | ElementLists.Add(nameof(MapElements), map => (IList) map.MapElements); 28 | } 29 | 30 | public ElementList Pins => ElementLists[nameof(Pins)]; 31 | 32 | public ElementList MapElements { 33 | get => ElementLists[nameof(MapElements)]; 34 | set => ElementLists[nameof(MapElements)] = value; 35 | } 36 | 37 | public MapSpan? VisibleRegion { 38 | set => SetValue(nameof(VisibleRegion), value, map => { 39 | if (value == null) 40 | return; 41 | map.MoveToRegion(new xf.Maps.MapSpan( 42 | new xf.Maps.Position(value.Center.Latitude, value.Center.Longitude), value.LatitudeDegrees, 43 | value.LongitudeDegrees)); 44 | }); 45 | } 46 | 47 | public MapType MapType { 48 | get => (MapType) GetValue(xf.Maps.Map.MapTypeProperty); 49 | set => SetValue(xf.Maps.Map.MapTypeProperty, (xf.Maps.MapType) value); 50 | } 51 | 52 | public bool IsShowingUser { 53 | get => GetValue(xf.Maps.Map.IsShowingUserProperty); 54 | set => SetValue(xf.Maps.Map.IsShowingUserProperty, value); 55 | } 56 | 57 | public bool TrafficEnabled { 58 | get => GetValue(xf.Maps.Map.TrafficEnabledProperty); 59 | set => SetValue(xf.Maps.Map.TrafficEnabledProperty, value); 60 | } 61 | 62 | public bool HasScrollEnabled { 63 | get => GetValue(xf.Maps.Map.HasScrollEnabledProperty); 64 | set => SetValue(xf.Maps.Map.HasScrollEnabledProperty, value); 65 | } 66 | 67 | public bool HasZoomEnabled { 68 | get => GetValue(xf.Maps.Map.HasZoomEnabledProperty); 69 | set => SetValue(xf.Maps.Map.HasZoomEnabledProperty, value); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /demo/app/GroupedCollectionView.cs: -------------------------------------------------------------------------------- 1 | using ChanceNET; 2 | 3 | namespace Laconic.Demo; 4 | 5 | static class GroupedCollectionView 6 | { 7 | static View SectionHeaderRow(string text) => new Grid 8 | { 9 | Padding = (15, 0), 10 | HeightRequest = 40, 11 | ["letter"] = new Label 12 | { 13 | Text = text, 14 | FontSize = 18, 15 | FontAttributes = FontAttributes.Bold, 16 | BackgroundColor = Color.Chocolate, 17 | TextColor = Color.White, 18 | WidthRequest = 40, 19 | HorizontalOptions = LayoutOptions.Start, 20 | VerticalTextAlignment = TextAlignment.Center, 21 | HorizontalTextAlignment = TextAlignment.Center, 22 | }, 23 | ["underline"] = new BoxView 24 | { 25 | BackgroundColor = Color.Chocolate, HeightRequest = 2, VerticalOptions = LayoutOptions.End 26 | } 27 | }; 28 | 29 | static View ItemRow(string name, string phone) => new StackLayout 30 | { 31 | Orientation = StackOrientation.Horizontal, 32 | Padding = (30, 0), 33 | HeightRequest = 30, 34 | ["name"] = new Label {Text = name}, 35 | ["phone"] = new Label 36 | { 37 | Text = phone, TextColor = Color.Gray, HorizontalOptions = LayoutOptions.EndAndExpand 38 | } 39 | }; 40 | 41 | // A helper function that converts a sequence "Alice", "Bob" 42 | // to "A", "Alice", "B", "Bob" and creates corresponding blueprints 43 | static IEnumerable<(string ReuseKey, string Key, View View)> GroupedItems(IEnumerable state) 44 | { 45 | var grouped = state.ToLookup(x => x.LastName.Substring(0, 1), x => x); 46 | foreach (var group in grouped.OrderBy(x => x.Key)) 47 | { 48 | yield return ("header", group.Key, SectionHeaderRow(group.Key.ToUpper())); 49 | foreach (var item in group.OrderBy(x => x.LastName)) 50 | yield return ("item", item.GetHashCode().ToString(), 51 | ItemRow(item.LastName + ", " + item.FirstName, item.Phone)); 52 | } 53 | } 54 | 55 | public static Person[] InitialState() 56 | { 57 | var chance = new Chance(); 58 | return Enumerable.Range(1, 200).Select(_ => chance.Person()).ToArray(); 59 | } 60 | 61 | public static StackLayout Content(IEnumerable state) => new() { 62 | ["list"] = new CollectionView 63 | { 64 | Items = GroupedItems(state).ToItemsList(x => x.ReuseKey, x => x.Key, x => x.View) 65 | } 66 | }; 67 | } -------------------------------------------------------------------------------- /maps/src/Distance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Laconic.Maps 4 | { 5 | /// Struct that encapsulates a distance (natively stored as a double of meters). 6 | /// To be added. 7 | public struct Distance 8 | { 9 | public Distance(double meters) => this.Meters = meters; 10 | 11 | public double Meters { get; } 12 | 13 | public double Miles => this.Meters / 1609.344; 14 | 15 | public double Kilometers => this.Meters / 1000.0; 16 | 17 | public static Distance FromMiles(double miles) 18 | { 19 | if (miles < 0.0) 20 | miles = 0.0; 21 | return new Distance(miles * 1609.344); 22 | } 23 | 24 | public static Distance FromMeters(double meters) 25 | { 26 | if (meters < 0.0) 27 | meters = 0.0; 28 | return new Distance(meters); 29 | } 30 | 31 | public static Distance FromKilometers(double kilometers) 32 | { 33 | if (kilometers < 0.0) 34 | kilometers = 0.0; 35 | return new Distance(kilometers * 1000.0); 36 | } 37 | 38 | public static Distance BetweenPositions(Position position1, Position position2) 39 | { 40 | var radians1 = position1.Latitude.ToRadians(); 41 | var radians2 = position1.Longitude.ToRadians(); 42 | var radians3 = position2.Latitude.ToRadians(); 43 | var radians4 = position2.Longitude.ToRadians(); 44 | var num1 = Math.Sin((radians3 - radians1) / 2.0); 45 | var num2 = num1 * num1; 46 | var num3 = radians2; 47 | var num4 = Math.Sin((radians4 - num3) / 2.0); 48 | var num5 = num4 * num4; 49 | var d = num2 + Math.Cos(radians1) * Math.Cos(radians3) * num5; 50 | return FromKilometers(12742.0 * Math.Atan2(Math.Sqrt(d), Math.Sqrt(1.0 - d))); 51 | } 52 | 53 | public bool Equals(Distance other) => this.Meters.Equals(other.Meters); 54 | 55 | public override bool Equals(object? obj) => obj is Distance other && Equals(other); 56 | 57 | public override int GetHashCode() => Meters.GetHashCode(); 58 | 59 | public static bool operator ==(Distance left, Distance right) => left.Equals(right); 60 | 61 | public static bool operator !=(Distance left, Distance right) => !left.Equals(right); 62 | } 63 | 64 | static class GeographyUtils 65 | { 66 | public static double ToRadians(this double degrees) => degrees * Math.PI / 180.0; 67 | 68 | public static double ToDegrees(this double radians) => radians / Math.PI * 180.0; 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/ElementListDiff.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | static class ElementListDiff 4 | { 5 | public static ListOperation[] Calculate(IDictionary? existingItems, 6 | IDictionary newItems) 7 | { 8 | var res = new List(); 9 | 10 | if (existingItems == null || existingItems.Count == 0) { 11 | foreach (var (key, el) in newItems.Where(p => p.Value != null)) { 12 | var childOps = Diff.Calculate(null, el).ToArray(); 13 | // TODO: Refactor. Reuse key shouldn't be necessary when it's not used 14 | res.Add(new AddChild(key, "", res.Count, el, childOps)); 15 | } 16 | } 17 | else { 18 | var listDiff = new ListDiff( 19 | existingItems.Where(x => x.Value != null).Select(x => x.Key), 20 | newItems.Where(p => p.Value != null).Select(p => p.Key)); 21 | 22 | var index = 0; 23 | foreach (var action in listDiff.Actions) { 24 | if (action.ActionType == ListDiffActionType.Add) { 25 | var newItem = newItems[action.DestinationItem]; 26 | var childOps = Diff.Calculate(null, newItem).ToArray(); 27 | res.Add(new AddChild(action.DestinationItem, "TODO: Refactor this", index, newItem!, childOps)); 28 | index++; 29 | } 30 | else if (action.ActionType == ListDiffActionType.Remove) { 31 | res.Add(new RemoveChild(index)); 32 | } 33 | else { 34 | var existingView = existingItems[action.SourceItem]; 35 | var newView = newItems[action.SourceItem]; 36 | if (existingView == null) { 37 | var items = Diff.Calculate(null, newView).ToArray(); 38 | res.Add(new AddChild(action.SourceItem, "TODO: Refactor this", index, newView, items)); 39 | } 40 | else if (existingView.GetType() != newView.GetType()) { 41 | var ops = Diff.Calculate(null, newView).ToArray(); 42 | res.Add(new ReplaceChild(index, newView, ops)); 43 | } 44 | else { 45 | var patch = Diff.Calculate(existingView, newView).ToArray(); 46 | if (patch.Any()) 47 | res.Add(new UpdateChild(action.DestinationItem, index, newView, patch)); 48 | } 49 | 50 | index++; 51 | } 52 | } 53 | } 54 | 55 | return res.ToArray(); 56 | } 57 | } -------------------------------------------------------------------------------- /src/CornerRadius.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public readonly struct CornerRadius : IConvert 4 | { 5 | readonly bool _isParameterized; 6 | 7 | public double TopLeft { get; } 8 | public double TopRight { get; } 9 | public double BottomLeft { get; } 10 | public double BottomRight { get; } 11 | 12 | CornerRadius(double uniformRadius) : this(uniformRadius, uniformRadius, uniformRadius, uniformRadius) 13 | { 14 | } 15 | 16 | CornerRadius(double topLeft, double topRight, double bottomLeft, double bottomRight) 17 | { 18 | _isParameterized = true; 19 | 20 | TopLeft = topLeft; 21 | TopRight = topRight; 22 | BottomLeft = bottomLeft; 23 | BottomRight = bottomRight; 24 | } 25 | 26 | public static implicit operator CornerRadius(double uniformRadius) => new(uniformRadius); 27 | 28 | public static implicit operator CornerRadius( 29 | (double topLeft, double topRight, double bottomLeft, double bottomRight) values) 30 | => new(values.topLeft, values.topRight, values.bottomLeft, values.bottomRight); 31 | 32 | bool Equals(CornerRadius other) 33 | { 34 | if (!_isParameterized && !other._isParameterized) 35 | return true; 36 | 37 | return TopLeft == other.TopLeft && TopRight == other.TopRight && BottomLeft == other.BottomLeft && 38 | BottomRight == other.BottomRight; 39 | } 40 | 41 | public override bool Equals(object? obj) 42 | { 43 | if (ReferenceEquals(null, obj)) 44 | return false; 45 | 46 | return obj is CornerRadius cornerRadius && Equals(cornerRadius); 47 | } 48 | 49 | public override int GetHashCode() 50 | { 51 | unchecked { 52 | var hashCode = TopLeft.GetHashCode(); 53 | hashCode = (hashCode * 397) ^ TopRight.GetHashCode(); 54 | hashCode = (hashCode * 397) ^ BottomLeft.GetHashCode(); 55 | hashCode = (hashCode * 397) ^ BottomRight.GetHashCode(); 56 | return hashCode; 57 | } 58 | } 59 | 60 | public static bool operator ==(CornerRadius left, CornerRadius right) => left.Equals(right); 61 | 62 | public static bool operator !=(CornerRadius left, CornerRadius right) => !left.Equals(right); 63 | 64 | public void Deconstruct(out double topLeft, out double topRight, out double bottomLeft, out double bottomRight) 65 | { 66 | topLeft = TopLeft; 67 | topRight = TopRight; 68 | bottomLeft = BottomLeft; 69 | bottomRight = BottomRight; 70 | } 71 | 72 | object IConvert.ToNative() => 73 | _isParameterized 74 | ? new Xamarin.Forms.CornerRadius(TopLeft, TopRight, BottomLeft, BottomRight) 75 | : new Xamarin.Forms.CornerRadius(TopLeft); 76 | } -------------------------------------------------------------------------------- /cli-template/src/ios/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/GridTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class GridTests 4 | { 5 | [Fact] 6 | public void should_set_RowDefinitions_from_string() 7 | { 8 | var grid = new Grid {RowDefinitions = "*, 2*, Auto, 30"}; 9 | 10 | grid.RowDefinitions.Count.ShouldBe(4); 11 | grid.RowDefinitions[0].Height.ShouldBe(xf.GridLength.Star); 12 | grid.RowDefinitions[1].Height.ShouldBe(new xf.GridLength(2, xf.GridUnitType.Star)); 13 | grid.RowDefinitions[2].Height.ShouldBe(xf.GridLength.Auto); 14 | grid.RowDefinitions[3].Height.ShouldBe(new xf.GridLength(30)); 15 | } 16 | 17 | [Fact] 18 | public void should_set_ColumnDefinitions_from_string() 19 | { 20 | var grid = new Grid {ColumnDefinitions = "2*, Auto, 30"}; 21 | 22 | grid.ColumnDefinitions.Count.ShouldBe(3); 23 | grid.ColumnDefinitions[0].Width.ShouldBe(new xf.GridLength(2, xf.GridUnitType.Star)); 24 | grid.ColumnDefinitions[1].Width.ShouldBe(xf.GridLength.Auto); 25 | grid.ColumnDefinitions[2].Width.ShouldBe(new xf.GridLength(30)); 26 | } 27 | 28 | [Fact] 29 | public void should_create_rows_in_real_view() 30 | { 31 | var grid = new xf.Grid(); 32 | Patch.Apply(grid, Diff.Calculate(null, new Grid {RowDefinitions = "*, 2*, Auto, 30"}), _ => { }); 33 | 34 | grid.RowDefinitions.Count.ShouldBe(4); 35 | grid.RowDefinitions[0].Height.ShouldBe(xf.GridLength.Star); 36 | grid.RowDefinitions[1].Height.ShouldBe(new xf.GridLength(2, xf.GridUnitType.Star)); 37 | grid.RowDefinitions[2].Height.ShouldBe(xf.GridLength.Auto); 38 | grid.RowDefinitions[3].Height.ShouldBe(new xf.GridLength(30)); 39 | } 40 | 41 | [Fact] 42 | public void should_set_ColumnDefinitions_in_real_view() 43 | { 44 | var grid = new xf.Grid(); 45 | Patch.Apply(grid, Diff.Calculate(null, new Grid {ColumnDefinitions = "2*, Auto, 30"}), _ => { }); 46 | 47 | grid.ColumnDefinitions.Count.ShouldBe(3); 48 | grid.ColumnDefinitions[0].Width.ShouldBe(new xf.GridLength(2, xf.GridUnitType.Star)); 49 | grid.ColumnDefinitions[1].Width.ShouldBe(xf.GridLength.Auto); 50 | grid.ColumnDefinitions[2].Width.ShouldBe(new xf.GridLength(30)); 51 | } 52 | 53 | [Fact] 54 | public void should_set_ColumnDefinitions_in_real_view2() 55 | { 56 | var grid = new xf.Grid(); 57 | Patch.Apply(grid, Diff.Calculate(null, new Grid {ColumnDefinitions = "Auto, *, Auto"}), _ => { }); 58 | 59 | grid.ColumnDefinitions.Count.ShouldBe(3); 60 | grid.ColumnDefinitions[0].Width.ShouldBe(xf.GridLength.Auto); 61 | grid.ColumnDefinitions[1].Width.ShouldBe(xf.GridLength.Star); 62 | grid.ColumnDefinitions[2].Width.ShouldBe(xf.GridLength.Auto); 63 | } 64 | } -------------------------------------------------------------------------------- /test/BehaviorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class BehaviorTests 4 | { 5 | class TestBehavior : Behavior 6 | { 7 | public bool IsAttached; 8 | readonly string _text; 9 | xf.Label? _label; 10 | 11 | public TestBehavior(string text) : base(text) => _text = text; 12 | 13 | protected internal override void OnValuesUpdated(object value) => _label!.Text = (string)value; 14 | 15 | protected internal override void OnAttachedTo(xf.Label bindable) 16 | { 17 | _label = bindable; 18 | IsAttached = true; 19 | bindable.Text = _text; 20 | } 21 | 22 | protected internal override void OnDetachingFrom(xf.Label bindable) => IsAttached = false; 23 | } 24 | 25 | [Fact] 26 | public void XF_Behavior_is_added_and_removed() 27 | { 28 | var real = new xf.Label(); 29 | var testBehavior = new TestBehavior(""); 30 | var originalBlueprint = new Label {Behaviors = {[0] = testBehavior}}; 31 | 32 | Patch.Apply(real, Diff.Calculate(null, originalBlueprint), _ => { }); 33 | 34 | real.Behaviors.Count.ShouldBe(1); 35 | testBehavior.IsAttached.ShouldBeTrue(); 36 | 37 | Patch.Apply(real, Diff.Calculate(originalBlueprint, new Label()), _ => { }); 38 | 39 | real.Behaviors.Count.ShouldBe(0); 40 | testBehavior.IsAttached.ShouldBeFalse(); 41 | } 42 | 43 | [Fact] 44 | public void Behavior_is_reused() 45 | { 46 | var real = new xf.Label(); 47 | var originalBlueprint = new Label {Behaviors = {[0] = new TestBehavior("") }}; 48 | 49 | Patch.Apply(real, Diff.Calculate(null, originalBlueprint), _ => { }); 50 | 51 | real.Behaviors.Count.ShouldBe(1); 52 | var realBehavior = real.Behaviors[0]; 53 | 54 | Patch.Apply(real, Diff.Calculate( 55 | originalBlueprint, 56 | new Label{Behaviors = {[0] = new TestBehavior("")}}), 57 | _ => { }); 58 | 59 | real.Behaviors[0].ShouldBe(realBehavior); 60 | } 61 | 62 | [Fact] 63 | public void Behavior_updates_view_values() 64 | { 65 | var real = new xf.Label(); 66 | 67 | var originalBlueprint = new Label {Behaviors = {[0] = new TestBehavior("")}}; 68 | Patch.Apply(real, Diff.Calculate(null, originalBlueprint), _ => { }); 69 | 70 | real.Text.ShouldBe(""); 71 | 72 | Patch.Apply(real, Diff.Calculate( 73 | originalBlueprint, 74 | new Label{Behaviors = {[0] = new TestBehavior("updated")}}), 75 | _ => { }); 76 | 77 | real.Text.ShouldBe("updated"); 78 | } 79 | } -------------------------------------------------------------------------------- /demo/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "scale": "2x", 5 | "size": "20x20", 6 | "idiom": "iphone", 7 | "filename": "Icon40.png" 8 | }, 9 | { 10 | "scale": "3x", 11 | "size": "20x20", 12 | "idiom": "iphone", 13 | "filename": "Icon60.png" 14 | }, 15 | { 16 | "scale": "2x", 17 | "size": "29x29", 18 | "idiom": "iphone", 19 | "filename": "Icon58.png" 20 | }, 21 | { 22 | "scale": "3x", 23 | "size": "29x29", 24 | "idiom": "iphone", 25 | "filename": "Icon87.png" 26 | }, 27 | { 28 | "scale": "2x", 29 | "size": "40x40", 30 | "idiom": "iphone", 31 | "filename": "Icon80.png" 32 | }, 33 | { 34 | "scale": "3x", 35 | "size": "40x40", 36 | "idiom": "iphone", 37 | "filename": "Icon120.png" 38 | }, 39 | { 40 | "scale": "2x", 41 | "size": "60x60", 42 | "idiom": "iphone", 43 | "filename": "Icon120.png" 44 | }, 45 | { 46 | "scale": "3x", 47 | "size": "60x60", 48 | "idiom": "iphone", 49 | "filename": "Icon180.png" 50 | }, 51 | { 52 | "scale": "1x", 53 | "size": "20x20", 54 | "idiom": "ipad", 55 | "filename": "Icon20.png" 56 | }, 57 | { 58 | "scale": "2x", 59 | "size": "20x20", 60 | "idiom": "ipad", 61 | "filename": "Icon40.png" 62 | }, 63 | { 64 | "scale": "1x", 65 | "size": "29x29", 66 | "idiom": "ipad", 67 | "filename": "Icon29.png" 68 | }, 69 | { 70 | "scale": "2x", 71 | "size": "29x29", 72 | "idiom": "ipad", 73 | "filename": "Icon58.png" 74 | }, 75 | { 76 | "scale": "1x", 77 | "size": "40x40", 78 | "idiom": "ipad", 79 | "filename": "Icon40.png" 80 | }, 81 | { 82 | "scale": "2x", 83 | "size": "40x40", 84 | "idiom": "ipad", 85 | "filename": "Icon80.png" 86 | }, 87 | { 88 | "scale": "1x", 89 | "size": "76x76", 90 | "idiom": "ipad", 91 | "filename": "Icon76.png" 92 | }, 93 | { 94 | "scale": "2x", 95 | "size": "76x76", 96 | "idiom": "ipad", 97 | "filename": "Icon152.png" 98 | }, 99 | { 100 | "scale": "2x", 101 | "size": "83.5x83.5", 102 | "idiom": "ipad", 103 | "filename": "Icon167.png" 104 | }, 105 | { 106 | "scale": "1x", 107 | "size": "1024x1024", 108 | "idiom": "ios-marketing", 109 | "filename": "Icon1024.png" 110 | } 111 | ], 112 | "properties": {}, 113 | "info": { 114 | "version": 1, 115 | "author": "xcode" 116 | } 117 | } -------------------------------------------------------------------------------- /codegen/test/RecordTests.cs: -------------------------------------------------------------------------------- 1 | using Laconic.CodeGeneration; 2 | using Xunit; 3 | 4 | namespace Laconic.CodeGeneration.Tests 5 | { 6 | // ReSharper disable UnusedType.Global 7 | // ReSharper disable UnusedMember.Global 8 | [Records] 9 | public interface TestRecords 10 | { 11 | record User(string firstName, string lastName); 12 | record TaggedUser(User user, params string[] tags); 13 | } 14 | // ReSharper restore UnusedType.Global 15 | // ReSharper restore UnusedMember.Global 16 | 17 | public class RecordTests 18 | { 19 | [Fact] 20 | public void parameters_become_property_names() 21 | { 22 | var user = new User("a", "b"); 23 | 24 | Assert.Equal("a", user.FirstName); 25 | Assert.Equal("b", user.LastName); 26 | } 27 | 28 | [Fact] 29 | public void With_method_works() 30 | { 31 | var user = new User("a", "b"); 32 | var updated = user.With(lastName: "c"); 33 | 34 | Assert.Equal("a", updated.FirstName); 35 | Assert.Equal("c", updated.LastName); 36 | } 37 | 38 | [Fact] 39 | public void Deconstruct_method_works() 40 | { 41 | var user = new User("a", "b"); 42 | var (first, last) = user; 43 | 44 | Assert.Equal("a", first); 45 | Assert.Equal("b", last); 46 | } 47 | 48 | [Fact] 49 | public void Records_have_structural_equality() 50 | { 51 | Assert.Equal(new User("a", "b"), new User("a", "b")); 52 | Assert.True(((object) new User("a", "b")).Equals((object) new User("a", "b"))); 53 | 54 | Assert.NotEqual(new User("a", "b"), new User("a", "c")); 55 | 56 | Assert.True(new User("a", "b") == new User("a", "b")); 57 | Assert.False(new User("a", "b") == new User("a", "c")); 58 | } 59 | 60 | [Fact] 61 | public void Records_define_GetHashCode() 62 | { 63 | var user1 = new User("a", "b"); 64 | var user2 = new User("a", "b"); 65 | var user3 = new User("a", "c"); 66 | 67 | Assert.Equal(user1.GetHashCode(), user2.GetHashCode()); 68 | Assert.NotEqual(user1.GetHashCode(), user3.GetHashCode()); 69 | } 70 | 71 | [Fact] 72 | public void Records_with_params_arrays_have_structural_equality() 73 | { 74 | Assert.Equal( 75 | new TaggedUser(new User("a", "b"), "t1", "t2"), 76 | new TaggedUser(new User("a", "b"), "t1", "t2")); 77 | 78 | Assert.NotEqual( 79 | new TaggedUser(new User("a", "b"), "t1", "t2"), 80 | new TaggedUser(new User("a", "b"), "t1", "t2", "t3")); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /maps/demo/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "scale": "2x", 5 | "size": "20x20", 6 | "idiom": "iphone", 7 | "filename": "Icon40.png" 8 | }, 9 | { 10 | "scale": "3x", 11 | "size": "20x20", 12 | "idiom": "iphone", 13 | "filename": "Icon60.png" 14 | }, 15 | { 16 | "scale": "2x", 17 | "size": "29x29", 18 | "idiom": "iphone", 19 | "filename": "Icon58.png" 20 | }, 21 | { 22 | "scale": "3x", 23 | "size": "29x29", 24 | "idiom": "iphone", 25 | "filename": "Icon87.png" 26 | }, 27 | { 28 | "scale": "2x", 29 | "size": "40x40", 30 | "idiom": "iphone", 31 | "filename": "Icon80.png" 32 | }, 33 | { 34 | "scale": "3x", 35 | "size": "40x40", 36 | "idiom": "iphone", 37 | "filename": "Icon120.png" 38 | }, 39 | { 40 | "scale": "2x", 41 | "size": "60x60", 42 | "idiom": "iphone", 43 | "filename": "Icon120.png" 44 | }, 45 | { 46 | "scale": "3x", 47 | "size": "60x60", 48 | "idiom": "iphone", 49 | "filename": "Icon180.png" 50 | }, 51 | { 52 | "scale": "1x", 53 | "size": "20x20", 54 | "idiom": "ipad", 55 | "filename": "Icon20.png" 56 | }, 57 | { 58 | "scale": "2x", 59 | "size": "20x20", 60 | "idiom": "ipad", 61 | "filename": "Icon40.png" 62 | }, 63 | { 64 | "scale": "1x", 65 | "size": "29x29", 66 | "idiom": "ipad", 67 | "filename": "Icon29.png" 68 | }, 69 | { 70 | "scale": "2x", 71 | "size": "29x29", 72 | "idiom": "ipad", 73 | "filename": "Icon58.png" 74 | }, 75 | { 76 | "scale": "1x", 77 | "size": "40x40", 78 | "idiom": "ipad", 79 | "filename": "Icon40.png" 80 | }, 81 | { 82 | "scale": "2x", 83 | "size": "40x40", 84 | "idiom": "ipad", 85 | "filename": "Icon80.png" 86 | }, 87 | { 88 | "scale": "1x", 89 | "size": "76x76", 90 | "idiom": "ipad", 91 | "filename": "Icon76.png" 92 | }, 93 | { 94 | "scale": "2x", 95 | "size": "76x76", 96 | "idiom": "ipad", 97 | "filename": "Icon152.png" 98 | }, 99 | { 100 | "scale": "2x", 101 | "size": "83.5x83.5", 102 | "idiom": "ipad", 103 | "filename": "Icon167.png" 104 | }, 105 | { 106 | "scale": "1x", 107 | "size": "1024x1024", 108 | "idiom": "ios-marketing", 109 | "filename": "Icon1024.png" 110 | } 111 | ], 112 | "properties": {}, 113 | "info": { 114 | "version": 1, 115 | "author": "xcode" 116 | } 117 | } -------------------------------------------------------------------------------- /cli-template/src/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "scale": "2x", 5 | "size": "20x20", 6 | "idiom": "iphone", 7 | "filename": "Icon40.png" 8 | }, 9 | { 10 | "scale": "3x", 11 | "size": "20x20", 12 | "idiom": "iphone", 13 | "filename": "Icon60.png" 14 | }, 15 | { 16 | "scale": "2x", 17 | "size": "29x29", 18 | "idiom": "iphone", 19 | "filename": "Icon58.png" 20 | }, 21 | { 22 | "scale": "3x", 23 | "size": "29x29", 24 | "idiom": "iphone", 25 | "filename": "Icon87.png" 26 | }, 27 | { 28 | "scale": "2x", 29 | "size": "40x40", 30 | "idiom": "iphone", 31 | "filename": "Icon80.png" 32 | }, 33 | { 34 | "scale": "3x", 35 | "size": "40x40", 36 | "idiom": "iphone", 37 | "filename": "Icon120.png" 38 | }, 39 | { 40 | "scale": "2x", 41 | "size": "60x60", 42 | "idiom": "iphone", 43 | "filename": "Icon120.png" 44 | }, 45 | { 46 | "scale": "3x", 47 | "size": "60x60", 48 | "idiom": "iphone", 49 | "filename": "Icon180.png" 50 | }, 51 | { 52 | "scale": "1x", 53 | "size": "20x20", 54 | "idiom": "ipad", 55 | "filename": "Icon20.png" 56 | }, 57 | { 58 | "scale": "2x", 59 | "size": "20x20", 60 | "idiom": "ipad", 61 | "filename": "Icon40.png" 62 | }, 63 | { 64 | "scale": "1x", 65 | "size": "29x29", 66 | "idiom": "ipad", 67 | "filename": "Icon29.png" 68 | }, 69 | { 70 | "scale": "2x", 71 | "size": "29x29", 72 | "idiom": "ipad", 73 | "filename": "Icon58.png" 74 | }, 75 | { 76 | "scale": "1x", 77 | "size": "40x40", 78 | "idiom": "ipad", 79 | "filename": "Icon40.png" 80 | }, 81 | { 82 | "scale": "2x", 83 | "size": "40x40", 84 | "idiom": "ipad", 85 | "filename": "Icon80.png" 86 | }, 87 | { 88 | "scale": "1x", 89 | "size": "76x76", 90 | "idiom": "ipad", 91 | "filename": "Icon76.png" 92 | }, 93 | { 94 | "scale": "2x", 95 | "size": "76x76", 96 | "idiom": "ipad", 97 | "filename": "Icon152.png" 98 | }, 99 | { 100 | "scale": "2x", 101 | "size": "83.5x83.5", 102 | "idiom": "ipad", 103 | "filename": "Icon167.png" 104 | }, 105 | { 106 | "scale": "1x", 107 | "size": "1024x1024", 108 | "idiom": "ios-marketing", 109 | "filename": "Icon1024.png" 110 | } 111 | ], 112 | "properties": {}, 113 | "info": { 114 | "version": 1, 115 | "author": "xcode" 116 | } 117 | } -------------------------------------------------------------------------------- /cli-template/src/app/App.cs: -------------------------------------------------------------------------------- 1 | // Laconic version of TipCalc example from https://www.mvvmcross.com/documentation/tutorials/tipcalc/the-tip-calc-tutorial?scroll=1892 2 | 3 | using System; 4 | using Laconic; 5 | 6 | namespace Laconic.Template 7 | { 8 | public class App : Xamarin.Forms.Application 9 | { 10 | // Everything the app displays and manipulates is kept in an immutable state: 11 | record State 12 | { 13 | public double SubTotal { get; init; } 14 | public double Generosity { get; init; } 15 | public double Tip => SubTotal * Generosity / 100.0; 16 | } 17 | 18 | // ... and this is a function to modify the state: 19 | // given the current state and a signal, calculate and return the new state: 20 | static State Reducer(State state, Signal signal) => signal switch { 21 | // Signal class provides Deconstruct method with two out params; usual usage is (ID, Payload) 22 | ("subtotal", string newVal) => state with {SubTotal = Double.Parse(newVal)}, 23 | ("generosity", double newVal) => state with {Generosity = newVal}, 24 | _ => state 25 | }; 26 | 27 | // ... given the current state, describe how the entire app's UI must look: 28 | static ContentPage UI(State state) => new() { 29 | Content = new StackLayout { 30 | Margin = 50, 31 | // ...for adding child views use Directory initialization syntax: 32 | ["lbl-sub"] = new Label {Text = "Subtotal"}, 33 | ["subtotal"] = new Entry { 34 | Text = state.SubTotal.ToString(), 35 | Keyboard = Keyboard.Numeric, 36 | // ... instead of event subscriptions write lambdas that return an instance of Signal. 37 | // When the user provides input Laconic will call those lambdas: 38 | TextChanged = e => new( /* ID: */"subtotal", /* Payload: */ e.NewTextValue) 39 | }, 40 | ["lbl-gen"] = new Label {Text = "Generosity"}, 41 | ["slider"] = new Slider { 42 | Maximum = 100, 43 | Value = state.Generosity, 44 | ValueChanged = e => new("generosity", e.NewValue) 45 | }, 46 | ["lbl-tip-cap"] = new Label {Text = "Tip to leave"}, 47 | ["lbl-tip"] = new Label {Text = state.Tip.ToString()} 48 | } 49 | }; 50 | 51 | Binder _binder; 52 | 53 | public App() 54 | { 55 | // ... finally, bind everything together: 56 | _binder = Binder.Create(new State {SubTotal = 100, Generosity = 10}, Reducer); 57 | MainPage = _binder.CreateElement(UI); 58 | } 59 | } 60 | } 61 | 62 | 63 | // Temporary stub: records won't work without this 64 | namespace System.Runtime.CompilerServices 65 | { 66 | sealed class IsExternalInit 67 | { 68 | } 69 | } -------------------------------------------------------------------------------- /src/ViewList.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic; 2 | 3 | public class ViewList : IDictionary 4 | { 5 | readonly Dictionary _internalStorage = new(); 6 | 7 | public ViewList() 8 | { 9 | } 10 | 11 | ViewList(IEnumerable<(Key, View?)> source) 12 | { 13 | foreach (var (key, view) in source) 14 | if (view != null) 15 | _internalStorage.Add(key, view); 16 | } 17 | 18 | public View? this[Key key] { 19 | get => _internalStorage[key]; 20 | set { 21 | if (value != null) 22 | _internalStorage[key] = value; 23 | } 24 | } 25 | 26 | public int Count => _internalStorage.Count; 27 | 28 | internal IEnumerable Keys => _internalStorage.Keys; 29 | 30 | ICollection IDictionary.Keys => _internalStorage.Keys; 31 | ICollection IDictionary.Values => throw new NotSupportedException(); 32 | 33 | int ICollection>.Count => _internalStorage.Count; 34 | 35 | bool ICollection>.IsReadOnly => throw new NotSupportedException(); 36 | 37 | public void Add(Key key, View? value) 38 | { 39 | if (value != null) 40 | _internalStorage.Add(key, value); 41 | } 42 | 43 | void ICollection>.Add(KeyValuePair item) => 44 | throw new NotSupportedException(); 45 | 46 | void ICollection>.Clear() => throw new NotSupportedException(); 47 | 48 | bool ICollection>.Contains(KeyValuePair item) => 49 | throw new NotSupportedException(); 50 | 51 | public bool ContainsKey(Key key) => _internalStorage.ContainsKey(key); 52 | 53 | void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => 54 | _internalStorage.ToArray(); 55 | 56 | IEnumerator> IEnumerable>.GetEnumerator() => 57 | _internalStorage.GetEnumerator(); 58 | 59 | IEnumerator IEnumerable.GetEnumerator() => _internalStorage.GetEnumerator(); 60 | 61 | bool IDictionary.Remove(Key key) => throw new NotSupportedException(); 62 | 63 | bool ICollection>.Remove(KeyValuePair item) => 64 | throw new NotSupportedException(); 65 | 66 | bool IDictionary.TryGetValue(Key key, out View value) => throw new NotSupportedException(); 67 | 68 | public static implicit operator ViewList(Dictionary source) => new(source.Select(x => (x.Key, x.Value))); 69 | 70 | public static implicit operator ViewList(Dictionary source) => new(source.Select(x => ((Key) x.Key, x.Value))); 71 | 72 | public static implicit operator ViewList(Dictionary source) => new(source.Select(x => ((Key) x.Key, x.Value))); 73 | 74 | public static implicit operator ViewList(Dictionary source) => new(source.Select(x => ((Key) x.Key, x.Value))); 75 | } -------------------------------------------------------------------------------- /test/CollectionViewTests.cs: -------------------------------------------------------------------------------- 1 | namespace Laconic.Tests; 2 | 3 | public class CollectionViewTests 4 | { 5 | [Fact] 6 | public void should_create_ItemsSource() 7 | { 8 | var colView = new xf.CollectionView(); 9 | Patch.Apply(colView, 10 | Diff.Calculate(null, 11 | new CollectionView 12 | { 13 | Items = {["key1"] = new Label {Text = "One"}, ["key2"] = new Label {Text = "Two"},} 14 | }), _ => { }); 15 | var source = (IList) colView.ItemsSource; 16 | 17 | source[0].Blueprint.ShouldBeOfType