diff --git a/.github/workflows/build-gtk.yml b/.github/workflows/build-gtk.yml new file mode 100644 index 000000000000..d0ab3db54bc6 --- /dev/null +++ b/.github/workflows/build-gtk.yml @@ -0,0 +1,53 @@ +name: Build Controls.Sample.Gtk +on: [push, pull_request] + +jobs: + build_and_test: + name: Build & Test + runs-on: ubuntu-22.04 + env: + GtkSharpVersion: 3.24.24.77-develop + DotnetVersion: 6.0.300 + steps: + - name: Checkout MAUI repo + uses: actions/checkout@v2 + # We also tested using 6.0.111 for both projects + # but MAUI failed to build on this version with this error: + # Could not load file or assembly 'Microsoft.CodeAnalysis, Version=4.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' + # So instead we use 6.0.300 + - name: Setup .NET SDK ${{ env.DotnetVersion }} + uses: actions/setup-dotnet@v1.7.2 + with: + dotnet-version: ${{ env.DotnetVersion }} + - name: Install gtk workload + run: | + dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/GtkSharp/index.json" + # For some reason automatic workload manifest detection doesn't work (see https://github.com/GtkSharp/GtkSharp/issues/355#issuecomment-1446262239), so download and uzip mainfest file manually + wget --user ${{ github.repository_owner }} --password ${{ secrets.GITHUB_TOKEN }} https://nuget.pkg.github.com/GtkSharp/download/gtksharp.net.sdk.gtk.manifest-${{ env.DotnetVersion }}/$GtkSharpVersion/gtksharp.net.sdk.gtk.manifest-${{ env.DotnetVersion }}.nupkg + DOTNET_DIR=/home/runner/.dotnet + WORKLOAD_MANIFEST_DIR=$DOTNET_DIR/sdk-manifests/${{ env.DotnetVersion }}/gtksharp.net.sdk.gtk + unzip -j gtksharp.net.sdk.gtk.manifest-${{ env.DotnetVersion }}.nupkg "data/*" -d $WORKLOAD_MANIFEST_DIR/ + rm gtksharp.net.sdk.gtk.manifest-${{ env.DotnetVersion }}.nupkg + + # otherwise we get System.UnauthorizedAccessException: Access to the path '/home/runner/.dotnet/sdk-manifests/6.0.300/gtksharp.net.sdk.gtk/WorkloadManifest.json' is denied. + chmod 764 $WORKLOAD_MANIFEST_DIR/* + + dotnet workload search + dotnet workload install gtk --skip-manifest-update + - name: Build MAUI + run: | + dotnet build src/Controls/src/SourceGen/Controls.SourceGen.csproj + dotnet build Microsoft.Maui.Gtk.slnf + + dotnet-format: + needs: build_and_test + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Run dotnet format + run: | + dotnet format whitespace ./src --folder --exclude Templates/src + git diff --exit-code + diff --git a/.github/workflows/dotnet-format-daily.yml b/.github/workflows/dotnet-format-daily.yml deleted file mode 100644 index eb1806e80ebf..000000000000 --- a/.github/workflows/dotnet-format-daily.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Daily code format check -on: - workflow_dispatch: - schedule: - - cron: 0 0 * * * # Every day at midnight (UTC) - -permissions: - pull-requests: write - contents: write - -jobs: - dotnet-format: - runs-on: windows-latest - steps: - - name: Checkout repo - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - - name: Run dotnet format - run: dotnet format whitespace ./src --folder --exclude Templates/src - - - name: Commit files - if: steps.format.outputs.has-changes == 'true' - run: | - git config --local user.name "github-actions[bot]" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -a -m 'Automated dotnet-format update' - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 - with: - title: '[housekeeping] Automated PR to fix formatting errors' - body: | - Automated PR to fix formatting errors - committer: GitHub - author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> - labels: t/housekeeping ♻︎ - assignees: rmarinho, jsuarezruiz - reviewers: rmarinho, jsuarezruiz - branch: housekeeping/fix-codeformatting - - # Pushing won't work to forked repos - # - name: Commit files - # if: steps.format.outputs.has-changes == 'true' - # run: | - # git config --local user.name "github-actions[bot]" - # git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - # git commit -a -m 'Automated dotnet-format update - # Co-authored-by: ${{ github.event.pull_request.user.login }} <${{ github.event.pull_request.user.id }}+${{ github.event.pull_request.user.login }}@users.noreply.github.com>' - - # - name: Push changes - # if: steps.format.outputs.has-changes == 'true' - # uses: ad-m/github-push-action@v0.6.0 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # branch: ${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/label-partner-issues.yml b/.github/workflows/label-partner-issues.yml deleted file mode 100644 index 28271be657f5..000000000000 --- a/.github/workflows/label-partner-issues.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: 'Label partner issues' - -# Available triggers: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows -on: - pull_request: - types: [opened] - issues: - types: [opened] - -jobs: - label-partner-issues: - runs-on: ubuntu-latest - - # Check out the repo into the workspace within the VM - steps: - - uses: actions/checkout@v2 - - # Run the labeler - - name: 'Apply labels to issue/PR based on who opened it' - id: issue-opener-labeler - uses: eilon/IssueOpenerLabeler@v1.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - repo: ${{ github.repository }} - issue: ${{ format('{0}{1}', github.event.issue.number, github.event.number) }} # Coalesce the issue/PR number into a single number diff --git a/Microsoft.Maui.Gtk.slnf b/Microsoft.Maui.Gtk.slnf new file mode 100644 index 000000000000..a0f319104baf --- /dev/null +++ b/Microsoft.Maui.Gtk.slnf @@ -0,0 +1,17 @@ +{ + "solution": { + "path": "Microsoft.Maui.sln", + "projects": [ + "src\\Graphics\\src\\Graphics\\Graphics.csproj", + "src\\Graphics\\src\\Graphics.Gtk\\Graphics.Gtk.csproj", + "src\\Controls\\src\\Build.Tasks\\Controls.Build.Tasks.csproj", + "src\\Essentials\\src\\Essentials.csproj", + "src\\Controls\\src\\Core\\Controls.Core.csproj", + "src\\Controls\\src\\Xaml\\Controls.Xaml.csproj", + "src\\Core\\src\\Core.csproj", + "src\\Compatibility\\Core\\src\\Compatibility.csproj", + "src\\SingleProject\\Resizetizer\\src\\Resizetizer.csproj", + "src\\Controls\\samples\\Controls.Sample.Gtk\\Controls.Sample.Gtk.csproj" + ] + } +} \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 125cf2881417..1f02451a9234 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + https://github.com/dotnet/installer b98928971106bd301a7827962ff6d46fb7c28fc1 diff --git a/eng/Versions.props b/eng/Versions.props index a780796064dc..f615da182149 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,7 +1,7 @@ - 6.0.401-servicing.22419.11 + 6.0.300 6.0.9 @@ -66,12 +66,12 @@ 0.9.0 0.5.13 1.2.0 - 3.24.24.64 + 3.24.24.77-develop - 6.0.400 + 6.0.300 6.0.400 6.0.300 6.0.300 diff --git a/global.json b/global.json index 73051f43a5dd..06f73be763d0 100644 --- a/global.json +++ b/global.json @@ -7,7 +7,7 @@ "Microsoft.Build.NoTargets": "3.3.0" }, "sdk": { - "version": "6.0.400", + "version": "6.0.300", "allowPrerelease": true, "rollForward": "latestMinor" } diff --git a/src/BlazorWebView/src/Maui/Gtk/BlazorWebViewHandler.cs b/src/BlazorWebView/src/Maui/Gtk/BlazorWebViewHandler.cs index ef0fe35d36f0..4810728f90dc 100644 --- a/src/BlazorWebView/src/Maui/Gtk/BlazorWebViewHandler.cs +++ b/src/BlazorWebView/src/Maui/Gtk/BlazorWebViewHandler.cs @@ -16,7 +16,7 @@ public partial class BlazorWebViewHandler : ViewHandler throw new NotSupportedException(); private void StartWebViewCoreIfPossible() { } - + #pragma warning disable CS0649 private WebViewManager? _webviewManager; #pragma warning restore CS0649 diff --git a/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/ExamplePage.cs b/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/ExamplePage.cs index 65e863a08832..ed7acd83bbb4 100644 --- a/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/ExamplePage.cs +++ b/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/ExamplePage.cs @@ -150,7 +150,10 @@ void SetupMauiLayoutButtonSpacing() var button2 = new Button { - Padding = new Thickness(10), Text = "Change the button!", BackgroundColor = Colors.Green, TextColor = Colors.Yellow, + Padding = new Thickness(10), + Text = "Change the button!", + BackgroundColor = Colors.Green, + TextColor = Colors.Yellow, }; button2.Clicked += (sender, args) => @@ -247,7 +250,10 @@ void SetupMauiLayout() var labelG = new Label { - Text = "this has gradient", Background = new RadialGradientBrush(new GradientStopCollection { new(Colors.Aqua, 0), new(Colors.Green, 10), }), Padding = new Thickness(30), Margin = new Thickness(10), + Text = "this has gradient", + Background = new RadialGradientBrush(new GradientStopCollection { new(Colors.Aqua, 0), new(Colors.Green, 10), }), + Padding = new Thickness(30), + Margin = new Thickness(10), }; verticalStack.Add(labelG); @@ -269,7 +275,10 @@ void SetupMauiLayout() var button2 = new Button() { - TextColor = Colors.Green, Text = "Hello I'm a button", BackgroundColor = Colors.Purple, Margin = new Thickness(12), + TextColor = Colors.Green, + Text = "Hello I'm a button", + BackgroundColor = Colors.Purple, + Margin = new Thickness(12), }; horizontalStack.Add(button); diff --git a/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/SimpleSampleGtkApplication.cs b/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/SimpleSampleGtkApplication.cs index 848e42e5d479..4a1d889e7e98 100644 --- a/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/SimpleSampleGtkApplication.cs +++ b/src/Controls/samples/Controls.Sample.Gtk/SimpleSampleApp/SimpleSampleGtkApplication.cs @@ -25,7 +25,6 @@ Widget OnTopContainerOverride(Widget nativePage) { var b = new Box(Orientation.Vertical, 0) { - Fill = true, Expand = true, Margin = 5, diff --git a/src/Controls/src/Core/Compatibility/Gtk/FontNamedSizeService.cs b/src/Controls/src/Core/Compatibility/Gtk/FontNamedSizeService.cs new file mode 100644 index 000000000000..99e91a57fac4 --- /dev/null +++ b/src/Controls/src/Core/Compatibility/Gtk/FontNamedSizeService.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; + +#pragma warning disable CS0612 // Type or member is obsolete +[assembly: Microsoft.Maui.Controls.Dependency(typeof(Microsoft.Maui.Controls.Compatibility.Platform.Gtk.FontNamedSizeService))] +// Type or member is obsolete + +namespace Microsoft.Maui.Controls.Compatibility.Platform.Gtk +{ + public class FontNamedSizeService : IFontNamedSizeService + { + public double GetNamedSize(NamedSize size, Type targetElementType, bool useOldSizes) + { + switch (size) + { + case NamedSize.Default: + return 11; + case NamedSize.Micro: + case NamedSize.Caption: + return 12; + case NamedSize.Medium: + return 17; + case NamedSize.Large: + return 22; + case NamedSize.Small: + case NamedSize.Body: + return 14; + case NamedSize.Header: + return 46; + case NamedSize.Subtitle: + return 20; + case NamedSize.Title: + return 24; + default: + throw new ArgumentOutOfRangeException(nameof(size)); + } + } + } +} + +#pragma warning restore CS0612 \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Shapes/Path/PathHandler.Gtk.cs b/src/Controls/src/Core/Handlers/Shapes/Path/PathHandler.Gtk.cs index 8e3e8c3f2886..c4e3d75a8749 100644 --- a/src/Controls/src/Core/Handlers/Shapes/Path/PathHandler.Gtk.cs +++ b/src/Controls/src/Core/Handlers/Shapes/Path/PathHandler.Gtk.cs @@ -5,8 +5,15 @@ namespace Microsoft.Maui.Controls.Handlers { public partial class PathHandler { - public static void MapShape(IShapeViewHandler handler, Path path) { } - public static void MapData(IShapeViewHandler handler, Path path) { } + public static void MapShape(IShapeViewHandler handler, Path path) + { + handler.PlatformView?.UpdateShape(path); + } + + public static void MapData(IShapeViewHandler handler, Path path) + { + handler.PlatformView?.UpdateShape(path); + } public static void MapRenderTransform(IShapeViewHandler handler, Path path) { } } } \ No newline at end of file diff --git a/src/Controls/src/Core/NavigationPage.cs b/src/Controls/src/Core/NavigationPage.cs index 577e0d728b7a..f39c60df1856 100644 --- a/src/Controls/src/Core/NavigationPage.cs +++ b/src/Controls/src/Core/NavigationPage.cs @@ -53,7 +53,7 @@ public partial class NavigationPage : Page, IPageContainer, IBarElement, I partial void Init(); -#if WINDOWS || ANDROID || TIZEN +#if WINDOWS || ANDROID || TIZEN || GTK const bool UseMauiHandler = true; #else const bool UseMauiHandler = false; diff --git a/src/Controls/src/Core/Platform/AlertManager/AlertMananger.Gtk.cs b/src/Controls/src/Core/Platform/AlertManager/AlertMananger.Gtk.cs index f8a05d4f5e47..45205fbc4071 100644 --- a/src/Controls/src/Core/Platform/AlertManager/AlertMananger.Gtk.cs +++ b/src/Controls/src/Core/Platform/AlertManager/AlertMananger.Gtk.cs @@ -1,19 +1,80 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Maui.Controls.Internals; namespace Microsoft.Maui.Controls.Platform { internal partial class AlertManager { - - [MissingMapper] - internal static void Subscribe(Window window) + readonly List _subscriptions = new List(); + + internal void Subscribe(Window window) { + var platformWindow = window.MauiContext.GetPlatformWindow(); + + if (_subscriptions.Any(s => s.PlatformWindow == platformWindow)) + return; + + _subscriptions.Add(new AlertRequestHelper(platformWindow)); + } + + internal void Unsubscribe(Window window) + { + var platformWindow = window.MauiContext.GetPlatformWindow(); + + var toRemove = _subscriptions.Where(sub => sub.PlatformWindow == platformWindow).ToList(); + + foreach (AlertRequestHelper alertRequestHelper in toRemove) + { + alertRequestHelper.Dispose(); + _subscriptions.Remove(alertRequestHelper); + } } - [MissingMapper] - internal static void Unsubscribe(Window window) + internal sealed class AlertRequestHelper : IDisposable { + public Gtk.Window PlatformWindow { get; } + + internal AlertRequestHelper(Gtk.Window window) + { + PlatformWindow = window; + + MessagingCenter.Subscribe(PlatformWindow, Page.BusySetSignalName, OnPageBusy); + MessagingCenter.Subscribe(PlatformWindow, Page.AlertSignalName, OnAlertRequested); + MessagingCenter.Subscribe(PlatformWindow, Page.PromptSignalName, OnPromptRequested); + MessagingCenter.Subscribe(PlatformWindow, Page.ActionSheetSignalName, OnActionSheetRequested); + } + + public void Dispose() + { + MessagingCenter.Unsubscribe(PlatformWindow, Page.BusySetSignalName); + MessagingCenter.Unsubscribe(PlatformWindow, Page.AlertSignalName); + MessagingCenter.Unsubscribe(PlatformWindow, Page.PromptSignalName); + MessagingCenter.Unsubscribe(PlatformWindow, Page.ActionSheetSignalName); + } + + void OnPageBusy(Page sender, bool enabled) + { + throw new NotImplementedException(); + } + + void OnAlertRequested(IView sender, AlertArguments arguments) + { + DialogHelper.ShowAlert(PlatformWindow, arguments); + } + + void OnPromptRequested(Page sender, PromptArguments arguments) + { + throw new NotImplementedException(); + } + void OnActionSheetRequested(Page sender, ActionSheetArguments arguments) + { + MainThread.BeginInvokeOnMainThread( + () => DialogHelper.ShowActionSheet(sender.Window.NativeWindow, arguments)); + } } } } diff --git a/src/Controls/src/Core/Platform/AlertManager/DialogHelper.Gtk.cs b/src/Controls/src/Core/Platform/AlertManager/DialogHelper.Gtk.cs new file mode 100644 index 000000000000..4b6a5bc894ce --- /dev/null +++ b/src/Controls/src/Core/Platform/AlertManager/DialogHelper.Gtk.cs @@ -0,0 +1,149 @@ +using System; +using System.Linq; +using GLib; +using Gtk; +using Microsoft.Maui.Controls.Internals; + + +namespace Microsoft.Maui.Controls.Platform +{ + // Ported from https://github.com/xamarin/Xamarin.Forms/blob/5.0.0/Xamarin.Forms.Platform.GTK/Helpers/DialogHelper.cs + internal static class DialogHelper + { + public static void ShowAlert(Gtk.Window window, AlertArguments arguments) + { + var messageDialog = new MessageDialog( + window, + DialogFlags.DestroyWithParent | DialogFlags.UseHeaderBar, + MessageType.Other, + GetAlertButtons(arguments), + arguments.Message + ); + + SetDialogTitle(arguments.Title, messageDialog); + SetButtonText(arguments.Accept, ResponseType.Ok, messageDialog); + SetButtonText(arguments.Cancel, ResponseType.Cancel, messageDialog); + + ResponseType result = (ResponseType)messageDialog.Run(); + + if (result == ResponseType.Ok) + { + arguments.SetResult(true); + } + else + { + arguments.SetResult(false); + } + + messageDialog.Destroy(); + } + + public static void ShowActionSheet(Gtk.Window window, ActionSheetArguments arguments) + { + // as title bar is not shown, use dialog body for title text + + var messageDialog = new MessageDialog( + window, + DialogFlags.DestroyWithParent | DialogFlags.UseHeaderBar, + MessageType.Other, + ButtonsType.Cancel, + arguments.Title + ); + + + //SetDialogTitle(arguments.Title, messageDialog); + SetButtonText(arguments.Cancel, ResponseType.Cancel, messageDialog); + SetDestructionButton(arguments.Destruction, messageDialog); + AddExtraButtons(arguments, messageDialog); + + var result = (ResponseType)messageDialog.Run(); + + if (result == ResponseType.Cancel) + { + arguments.SetResult(arguments.Cancel); + } + else if (result == ResponseType.Reject) + { + arguments.SetResult(arguments.Destruction); + } + + messageDialog.Destroy(); + } + + private static void SetDialogTitle(string title, MessageDialog messageDialog) + { + messageDialog.Title = title ?? string.Empty; + } + + private static void SetButtonText(string text, ResponseType type, MessageDialog messageDialog) + { + var button = messageDialog.GetWidgetForResponse((int)type) as Gtk.Button; + + if (button is null) + return; + + if (string.IsNullOrEmpty(text)) + { + button.Hide(); + } + else + { + button.Label = text; + } + } + + private static void SetDestructionButton(string destruction, MessageDialog messageDialog) + { + if (!string.IsNullOrEmpty(destruction)) + { + var destructionButton = + messageDialog.AddButton(destruction, ResponseType.Reject) as Gtk.Button; + + if (destructionButton is null) + return; + + var destructionColor = Microsoft.Maui.Graphics.Colors.Red; + destructionButton.Child.SetForegroundColor(destructionColor); + } + } + + private static void AddExtraButtons(ActionSheetArguments arguments, MessageDialog messageDialog) + { + foreach (var buttonText in arguments.Buttons) + { + var button = new Gtk.Button(); + button.Label = buttonText; + button.Clicked += (obj, eventArgs) => + { + arguments.SetResult(button.Label); + messageDialog.Destroy(); + }; + button.Show(); + messageDialog.AddActionWidget(button, ResponseType.None); + } + } + + private static ButtonsType GetAlertButtons(AlertArguments arguments) + { + bool hasAccept = !string.IsNullOrEmpty(arguments.Accept); + bool hasCancel = !string.IsNullOrEmpty(arguments.Cancel); + + ButtonsType type = ButtonsType.None; + + if (hasAccept && hasCancel) + { + type = ButtonsType.OkCancel; + } + else if (hasAccept && !hasCancel) + { + type = ButtonsType.Ok; + } + else if (!hasAccept && hasCancel) + { + type = ButtonsType.Cancel; + } + + return type; + } + } +} diff --git a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs index 6275ef1b866d..b1220230187c 100644 --- a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs +++ b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Gtk.cs @@ -1,17 +1,268 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Gtk; +using Microsoft.Maui.Controls.Internals; namespace Microsoft.Maui.Controls.Platform { + // ported from https://github.com/xamarin/Xamarin.Forms/blob/5.0.0/Xamarin.Forms.Platform.GTK/VisualElementTracker.cs class GestureManager : IDisposable { + readonly IPlatformViewHandler _handler; + readonly NotifyCollectionChangedEventHandler _collectionChangedHandler; + Widget? _container; + Widget? _control; + VisualElement? _element; + + bool _isDisposed; + public GestureManager(IViewHandler handler) { + _handler = (IPlatformViewHandler)handler; + _collectionChangedHandler = ModelGestureRecognizersOnCollectionChanged; + + if (_handler.VirtualView == null) + throw new ArgumentNullException(nameof(handler.VirtualView)); + + if (_handler.PlatformView == null) + throw new ArgumentNullException(nameof(handler.PlatformView)); + + Element = (VisualElement)_handler.VirtualView; + Control = _handler.PlatformView; + + if (_handler.ContainerView != null) + Container = _handler.ContainerView; + else + Container = _handler.PlatformView; + } + + public Widget? Container + { + get { return _container; } + set + { + if (_container == value) + return; + + if (_container != null) + { + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + } + + _container = value; + + UpdatingGestureRecognizers(); + } + } + + public Widget? Control + { + get { return _control; } + set + { + if (_control == value) + return; + + if (_control != null) + { + _control.ButtonPressEvent -= OnControlButtonPressEvent; + } + + _control = value; + + if (PreventGestureBubbling) + { + UpdatingGestureRecognizers(); + } + } + } + + public VisualElement? Element + { + get { return _element; } + set + { + if (_element == value) + return; + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var oldRecognizers = (ObservableCollection)view.GestureRecognizers; + oldRecognizers.CollectionChanged -= _collectionChangedHandler; + } + } + + _element = value; + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var newRecognizers = (ObservableCollection)view.GestureRecognizers; + newRecognizers.CollectionChanged += _collectionChangedHandler; + } + } + } + } + + private bool PreventGestureBubbling + { + get + { + return Element switch + { + Button => true, + CheckBox => true, + DatePicker => true, + Stepper => true, + Slider => true, + Switch => true, + TimePicker => true, + ImageButton => true, + RadioButton => true, + _ => false, + }; + } } public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + return; + + _isDisposed = true; + + if (!disposing) + return; + + if (_container != null) + { + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + } + + if (_element != null) + { + var view = _element as View; + if (view != null) + { + var oldRecognizers = (ObservableCollection)view.GestureRecognizers; + oldRecognizers.CollectionChanged -= _collectionChangedHandler; + } + } + + if (_control != null) + { + _control.ButtonPressEvent -= OnControlButtonPressEvent; + } + + Container?.Destroy(); + Container = null; + } + + private void ModelGestureRecognizersOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) + { + UpdatingGestureRecognizers(); + } + + private void UpdatingGestureRecognizers() + { + var view = Element as View; + var gestures = view?.GestureRecognizers; + + if (_container == null || gestures == null) + return; + + _container.ButtonPressEvent -= OnContainerButtonPressEvent; + + if (gestures.GetGesturesFor().Any() || gestures.GetGesturesFor().Any()) + { + _container.ButtonPressEvent += OnContainerButtonPressEvent; + } + else + { + if (_control != null && PreventGestureBubbling) + { + _control.ButtonPressEvent += OnControlButtonPressEvent; + } + } } + + private void OnContainerButtonPressEvent(object sender, ButtonPressEventArgs args) + { + var button = args.Event.Button; + if (button != 1 && button != 3) + { + return; + } + + var view = Element as View; + + if (view == null) + return; + + int numClicks = 0; + switch (args.Event.Type) + { + case Gdk.EventType.ThreeButtonPress: + numClicks = 3; + break; + case Gdk.EventType.TwoButtonPress: + numClicks = 2; + break; + case Gdk.EventType.ButtonPress: + numClicks = 1; + break; + default: + return; + } + + // Taps or Clicks + if (button == (uint)ButtonsMask.Primary) + { + IEnumerable tapGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfTapsRequired == numClicks); + + foreach (TapGestureRecognizer recognizer in tapGestures) + recognizer.SendTapped(view); + + IEnumerable clickGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfClicksRequired == numClicks && + recognizer.Buttons == ButtonsMask.Primary); + + foreach (ClickGestureRecognizer recognizer in clickGestures) + recognizer.SendClicked(view, ButtonsMask.Primary); + } + else + { + // right click + IEnumerable rightClickGestures = view.GestureRecognizers + .GetGesturesFor(recognizer => recognizer.NumberOfClicksRequired == numClicks && + recognizer.Buttons == ButtonsMask.Secondary); + + foreach (ClickGestureRecognizer recognizer in rightClickGestures) + recognizer.SendClicked(view, ButtonsMask.Secondary); + } + } + + private void OnControlButtonPressEvent(object sender, ButtonPressEventArgs args) + { + args.RetVal = true; + } + } } diff --git a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs index 1eb772b308c5..13e717c1ffe1 100644 --- a/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs @@ -23,6 +23,10 @@ #elif TIZEN using Microsoft.Maui.Controls.Handlers.Compatibility; using Microsoft.Maui.Controls.Compatibility.Platform.Tizen; +#elif GTK +using Microsoft.Maui.Controls.Handlers.Compatibility; +using Microsoft.Maui.Controls.Compatibility.Platform.Gtk; +using Microsoft.Maui.Graphics.Platform.Gtk; #endif namespace Microsoft.Maui.Controls.Hosting @@ -132,7 +136,7 @@ public static IMauiHandlersCollection AddMauiControlsHandlers(this IMauiHandlers handlersCollection.AddHandler(); #endif #endif -#if WINDOWS || ANDROID || TIZEN +#if WINDOWS || ANDROID || TIZEN || GTK handlersCollection.AddHandler(); handlersCollection.AddHandler(); handlersCollection.AddHandler(); @@ -163,6 +167,10 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder) #pragma warning restore CS0612, CA1416 // Type or member is obsolete #endif +#if GTK + DependencyService.Register(); +#endif + builder.ConfigureImageSourceHandlers(); builder .ConfigureMauiHandlers(handlers => @@ -174,6 +182,7 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder) builder.Services.TryAddEnumerable(ServiceDescriptor.Transient()); #endif builder.RemapForControls(); + AddDefaultMappings(); return builder; } @@ -242,5 +251,34 @@ internal static MauiAppBuilder RemapForControls(this MauiAppBuilder builder) return builder; } + + internal static void AddDefaultMappings() + { +#if GTK + ContentViewHandler.Mapper.AppendToMapping( + "BorderColor", + (IContentViewHandler handler, IContentView view) => + { + if (view is Frame frame) + { + Microsoft.Maui.Platform.ContentView contentView = (Microsoft.Maui.Platform.ContentView)handler.PlatformView; + contentView.SetStyleValueNode($"1px solid {frame.BorderColor.ToGdkRgba().ToString()}", contentView.CssMainNode(), "border"); + } + } + ); + + ContentViewHandler.Mapper.AppendToMapping( + "CornerRadius", + (IContentViewHandler handler, IContentView view) => + { + if (view is Frame frame) + { + Microsoft.Maui.Platform.ContentView contentView = (Microsoft.Maui.Platform.ContentView)handler.PlatformView; + contentView.SetStyleValueNode($"{frame.CornerRadius:f}px", contentView.CssMainNode(), "border-radius"); + } + } + ); +#endif + } } } \ No newline at end of file diff --git a/src/Core/src/Core/ILabel.cs b/src/Core/src/Core/ILabel.cs index cae833afa225..e2fc4084b1d0 100644 --- a/src/Core/src/Core/ILabel.cs +++ b/src/Core/src/Core/ILabel.cs @@ -5,6 +5,16 @@ namespace Microsoft.Maui /// public interface ILabel : IView, IText, ITextAlignment, IPadding { + /// + /// Gets the option for line breaking. + /// + LineBreakMode LineBreakMode { get; } + + /// + /// Gets the maximum number of lines allowed in the Label. + /// + int MaxLines { get; } + /// /// Gets the text decoration applied to the Label. /// Underline and strike-through text decorations can be applied. diff --git a/src/Core/src/Fonts/EmbeddedFontLoader.Gtk.cs b/src/Core/src/Fonts/EmbeddedFontLoader.Gtk.cs index b6eee0c86689..7d21de574a66 100644 --- a/src/Core/src/Fonts/EmbeddedFontLoader.Gtk.cs +++ b/src/Core/src/Fonts/EmbeddedFontLoader.Gtk.cs @@ -2,20 +2,20 @@ namespace Microsoft.Maui { - public partial class EmbeddedFontLoader + public partial class EmbeddedFontLoader { public string? LoadFont(EmbeddedFont font) { // gtk has no methods to load fonts // see: http://mces.blogspot.com/2015/05/how-to-use-custom-application-fonts.html - + throw new NotImplementedException(); - + // freetype is needed. look at: // https://github.com/Robmaister/SharpFont - - - + + + } } } diff --git a/src/Core/src/Fonts/FontManager.Gtk.cs b/src/Core/src/Fonts/FontManager.Gtk.cs index 6b99c2e8e40f..8cd1ed9ceab4 100644 --- a/src/Core/src/Fonts/FontManager.Gtk.cs +++ b/src/Core/src/Fonts/FontManager.Gtk.cs @@ -119,11 +119,11 @@ private FontDescription[] GetAvailableFontStyles() return styles.ToArray(); } - - internal static bool AddFontFile (string fontPath) + + internal static bool AddFontFile(string fontPath) { // Try to add font file to the current fontconfig configuration - var result = FcConfigAppFontAddFile (System.IntPtr.Zero, fontPath); + var result = FcConfigAppFontAddFile(System.IntPtr.Zero, fontPath); if (result) { diff --git a/src/Core/src/Fonts/IFontManager.Gtk.cs b/src/Core/src/Fonts/IFontManager.Gtk.cs index e13507e1a33f..f0b40dca58c0 100644 --- a/src/Core/src/Fonts/IFontManager.Gtk.cs +++ b/src/Core/src/Fonts/IFontManager.Gtk.cs @@ -4,7 +4,7 @@ namespace Microsoft.Maui { public partial interface IFontManager { - + FontDescription DefaultFontFamily { get; } FontDescription GetFontFamily(Font font); diff --git a/src/Core/src/Graphics/PaintExtensions.Gtk.cs b/src/Core/src/Graphics/PaintExtensions.Gtk.cs index 0f1b3dadb9ce..4040f0920b6f 100644 --- a/src/Core/src/Graphics/PaintExtensions.Gtk.cs +++ b/src/Core/src/Graphics/PaintExtensions.Gtk.cs @@ -20,22 +20,22 @@ public static partial class PaintExtensions switch (paint) { case ImagePaint { Image: GtkImage image } imagePaint: - { - var pixbuf = image.NativeImage; + { + var pixbuf = image.NativeImage; - return pixbuf; - } + return pixbuf; + } case PatternPaint patternPaint: - { + { - var pixbuf = patternPaint.GetPatternBitmap(1); - owned = true; + var pixbuf = patternPaint.GetPatternBitmap(1); + owned = true; - // TODO: create a cairo.pattern & store it in pixbuf - return pixbuf; + // TODO: create a cairo.pattern & store it in pixbuf + return pixbuf; - } + } } @@ -66,19 +66,19 @@ string Stops(PaintGradientStop[] sorted) switch (paint) { case LinearGradientPaint lg: - { - var stops = Stops(lg.GetSortedStops()); - var css = $"linear-gradient( to right, {stops})"; + { + var stops = Stops(lg.GetSortedStops()); + var css = $"linear-gradient( to right, {stops})"; - return css; - } + return css; + } case RadialGradientPaint rg: - { - var stops = Stops(rg.GetSortedStops()); - var css = $"radial-gradient({stops})"; + { + var stops = Stops(rg.GetSortedStops()); + var css = $"radial-gradient({stops})"; - return css; - } + return css; + } } diff --git a/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Gtk.cs b/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Gtk.cs index 0f99dc0150e9..883ae4856e45 100644 --- a/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Gtk.cs +++ b/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Gtk.cs @@ -14,11 +14,11 @@ public static void MapIsRunning(IActivityIndicatorHandler handler, IActivityIndi handler.PlatformView?.UpdateIsRunning(activityIndicator); } - + public static void MapColor(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator) { handler.PlatformView?.SetForegroundColor(activityIndicator.Color); - + } } } diff --git a/src/Core/src/Handlers/CheckBox/CheckBoxHandler.Gtk.cs b/src/Core/src/Handlers/CheckBox/CheckBoxHandler.Gtk.cs index 382a2ac54a15..79989d82e417 100644 --- a/src/Core/src/Handlers/CheckBox/CheckBoxHandler.Gtk.cs +++ b/src/Core/src/Handlers/CheckBox/CheckBoxHandler.Gtk.cs @@ -5,7 +5,7 @@ namespace Microsoft.Maui.Handlers { // https://docs.gtk.org/gtk3/class.CheckButton.html - + public partial class CheckBoxHandler : ViewHandler { diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Gtk.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Gtk.cs index fb023fa5e7a2..ed54218b26de 100644 --- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Gtk.cs +++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Gtk.cs @@ -11,23 +11,42 @@ protected override MauiDatePicker CreatePlatformView() return new MauiDatePicker(); } - [MissingMapper] + protected override void ConnectHandler(MauiDatePicker platformView) + { + base.ConnectHandler(platformView); + platformView.DateChanged += DateChanged; + } + + protected override void DisconnectHandler(MauiDatePicker platformView) + { + base.DisconnectHandler(platformView); + platformView.DateChanged -= DateChanged; + } + + private void DateChanged(object? sender, System.EventArgs args) + { + VirtualView.Date = PlatformView.Date; + } + public static void MapFormat(IDatePickerHandler handler, IDatePicker datePicker) { handler.PlatformView?.UpdateFormat(datePicker); } - [MissingMapper] public static void MapDate(IDatePickerHandler handler, IDatePicker datePicker) { handler.PlatformView?.UpdateDate(datePicker); } - [MissingMapper] - public static void MapMinimumDate(IDatePickerHandler handler, IDatePicker datePicker) { } + public static void MapMinimumDate(IDatePickerHandler handler, IDatePicker datePicker) + { + handler.PlatformView.MinDate = handler.VirtualView.MinimumDate; + } - [MissingMapper] - public static void MapMaximumDate(IDatePickerHandler handler, IDatePicker datePicker) { } + public static void MapMaximumDate(IDatePickerHandler handler, IDatePicker datePicker) + { + handler.PlatformView.MaxDate = handler.VirtualView.MaximumDate; + } [MissingMapper] public static void MapCharacterSpacing(IDatePickerHandler handler, IDatePicker datePicker) { } diff --git a/src/Core/src/Handlers/Editor/EditorHandler.Gtk.cs b/src/Core/src/Handlers/Editor/EditorHandler.Gtk.cs index e543551014b9..794d8f864039 100644 --- a/src/Core/src/Handlers/Editor/EditorHandler.Gtk.cs +++ b/src/Core/src/Handlers/Editor/EditorHandler.Gtk.cs @@ -51,7 +51,8 @@ protected void OnNativeTextChanged(object? sender, EventArgs e) if (PlatformView is not { } nativeView || VirtualView is not { } virtualView) return; - if (sender != nativeView.Buffer) return; + if (sender != nativeView.Buffer) + return; var text = nativeView.Buffer.Text; diff --git a/src/Core/src/Handlers/IViewHandler.Gtk.cs b/src/Core/src/Handlers/IViewHandler.Gtk.cs index 4e265560585b..66202c520572 100644 --- a/src/Core/src/Handlers/IViewHandler.Gtk.cs +++ b/src/Core/src/Handlers/IViewHandler.Gtk.cs @@ -3,7 +3,7 @@ public interface IPlatformViewHandler : IViewHandler { new Gtk.Widget? PlatformView { get; } - + new Gtk.Widget? ContainerView { get; } } diff --git a/src/Core/src/Handlers/Image/ImageHandler.Gtk.cs b/src/Core/src/Handlers/Image/ImageHandler.Gtk.cs index 4c5abd7a3a77..419460d07730 100644 --- a/src/Core/src/Handlers/Image/ImageHandler.Gtk.cs +++ b/src/Core/src/Handlers/Image/ImageHandler.Gtk.cs @@ -35,8 +35,41 @@ public static async Task MapSourceAsync(IImageHandler handler, IImage image) } - [MissingMapper] - void OnSetImageSource(Pixbuf? obj) => throw new NotImplementedException(); + + static int? Request(double viewSize) => viewSize >= 0 ? (int)Math.Round(viewSize) : null; + private void UpdateWidthRequest(IImage image) + { + var widthRequest = Request(image.Width * PlatformView.ScaleFactor); + + if (widthRequest is not null && widthRequest != PlatformView.WidthRequest && widthRequest != PlatformView.AllocatedWidth) + { + PlatformView.ChangeWidth(widthRequest.Value); + PlatformView.QueueResize(); + } + } + + private void UpdateHeightRequest(IImage image) + { + var heightRequest = Request(image.Height * PlatformView.ScaleFactor); + + if (heightRequest is not null && heightRequest != PlatformView.WidthRequest && heightRequest != PlatformView.AllocatedWidth) + { + PlatformView.ChangeHeight(heightRequest.Value); + PlatformView.QueueResize(); + } + } + + public static void MapWidthRequest(IImageHandler handler, IImage view) + { + ((ImageHandler)handler).UpdateWidthRequest(view); + } + + public static void MapHeightRequest(IImageHandler handler, IImage view) + { + ((ImageHandler)handler).UpdateHeightRequest(view); + } + + void OnSetImageSource(Pixbuf? obj) => PlatformView.Image = obj; } diff --git a/src/Core/src/Handlers/Image/ImageHandler.cs b/src/Core/src/Handlers/Image/ImageHandler.cs index f1ddf5ad5e47..c073567e5438 100644 --- a/src/Core/src/Handlers/Image/ImageHandler.cs +++ b/src/Core/src/Handlers/Image/ImageHandler.cs @@ -26,6 +26,10 @@ public partial class ImageHandler : IImageHandler [nameof(IImage.Aspect)] = MapAspect, [nameof(IImage.IsAnimationPlaying)] = MapIsAnimationPlaying, [nameof(IImage.Source)] = MapSource, +#if GTK + ["WidthRequest"] = MapWidthRequest, + ["HeightRequest"] = MapHeightRequest, +#endif }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) diff --git a/src/Core/src/Handlers/Label/LabelHandler.Gtk.cs b/src/Core/src/Handlers/Label/LabelHandler.Gtk.cs index 365b5c86086a..166e4bef6393 100644 --- a/src/Core/src/Handlers/Label/LabelHandler.Gtk.cs +++ b/src/Core/src/Handlers/Label/LabelHandler.Gtk.cs @@ -16,7 +16,8 @@ public partial class LabelHandler : ViewHandler PlatformStringSizeService stringSizeService => _stringSizeService ??= new(); public Microsoft.Maui.Graphics.Platform.Gtk.TextLayout SharedTextLayout => _textLayout ??= new Microsoft.Maui.Graphics.Platform.Gtk.TextLayout( - stringSizeService.SharedContext) { HeightForWidth = true }; + stringSizeService.SharedContext) + { HeightForWidth = true }; // https://docs.gtk.org/gtk3/class.Label.html protected override LabelView CreatePlatformView() @@ -56,7 +57,7 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra SharedTextLayout.HorizontalAlignment = virtualView.HorizontalTextAlignment.GetHorizontalAlignment(); SharedTextLayout.VerticalAlignment = virtualView.VerticalTextAlignment.GetVerticalAlignment(); - // SharedTextLayout.LineBreakMode = virtualView.LineBreakMode.GetLineBreakMode(); + SharedTextLayout.LineBreakMode = virtualView.LineBreakMode.GetLineBreakMode(); var heightForWidth = !heightConstrained; diff --git a/src/Core/src/Handlers/Label/LabelHandler.cs b/src/Core/src/Handlers/Label/LabelHandler.cs index 315741c8e1e9..b9a8186c7aff 100644 --- a/src/Core/src/Handlers/Label/LabelHandler.cs +++ b/src/Core/src/Handlers/Label/LabelHandler.cs @@ -39,6 +39,10 @@ public partial class LabelHandler : ILabelHandler [nameof(ILabel.Text)] = MapText, [nameof(ITextStyle.TextColor)] = MapTextColor, [nameof(ILabel.TextDecorations)] = MapTextDecorations, +#if GTK + [nameof(ILabel.LineBreakMode)] = MapLineBreakMode, + [nameof(ILabel.MaxLines)] = MapMaxLines, +#endif }; public static CommandMapper CommandMapper = new(ViewCommandMapper) diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.Gtk.cs b/src/Core/src/Handlers/Layout/LayoutHandler.Gtk.cs index 002bbd93dbef..349beb05ed8f 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.Gtk.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.Gtk.cs @@ -124,9 +124,21 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra #endif + private void UpdateVisibility(Visibility visibility) + { + if (visibility == Visibility.Visible) + PlatformView.Show(); + else + PlatformView.Hide(); + } + [MissingMapper] public void UpdateZIndex(IView view) => throw new NotImplementedException(); - + + static void MapVisibility(ILayoutHandler handler, ILayout layout) + { + ((LayoutHandler)handler).UpdateVisibility(layout.Visibility); + } } -} \ No newline at end of file +} diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.cs b/src/Core/src/Handlers/Layout/LayoutHandler.cs index fae19d866735..6057f5bdcc1c 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.cs @@ -23,6 +23,9 @@ public partial class LayoutHandler : ILayoutHandler [nameof(ILayout.ClipsToBounds)] = MapClipsToBounds, #if ANDROID || WINDOWS [nameof(IView.InputTransparent)] = MapInputTransparent, +#endif +#if GTK + [nameof(IView.Visibility)] = MapVisibility, #endif }; diff --git a/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Gtk.cs b/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Gtk.cs index a13e897b3acb..f58289666609 100644 --- a/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Gtk.cs +++ b/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Gtk.cs @@ -13,28 +13,28 @@ public partial class NavigationViewHandler : ViewHandler PlatformView?.Entry; - + public static void MapFont(ISearchBarHandler handler, ISearchBar searchBar) { var fontManager = handler.GetRequiredService(); @@ -68,7 +68,7 @@ public static void MapHorizontalTextAlignment(ISearchBarHandler handler, ISearch public static void MapVerticalTextAlignment(ISearchBarHandler handler, ISearchBar searchBar) { } - + public static void MapTextColor(ISearchBarHandler handler, ISearchBar searchBar) { handler.PlatformView?.Entry?.UpdateTextColor(searchBar.TextColor); diff --git a/src/Core/src/Handlers/ShapeView/ShapeViewHandler.Gtk.cs b/src/Core/src/Handlers/ShapeView/ShapeViewHandler.Gtk.cs index c329d66ca016..61a7fa666d97 100644 --- a/src/Core/src/Handlers/ShapeView/ShapeViewHandler.Gtk.cs +++ b/src/Core/src/Handlers/ShapeView/ShapeViewHandler.Gtk.cs @@ -44,7 +44,7 @@ public static void MapStrokeDashOffset(IShapeViewHandler handler, IShapeView sha { handler.PlatformView?.InvalidateShape(shapeView); } - + public static void MapStrokeLineCap(IShapeViewHandler handler, IShapeView shapeView) { handler.PlatformView?.InvalidateShape(shapeView); diff --git a/src/Core/src/Handlers/Stepper/StepperHandler.Gtk.cs b/src/Core/src/Handlers/Stepper/StepperHandler.Gtk.cs index 0876c5043599..1b84ea8a868c 100644 --- a/src/Core/src/Handlers/Stepper/StepperHandler.Gtk.cs +++ b/src/Core/src/Handlers/Stepper/StepperHandler.Gtk.cs @@ -3,7 +3,7 @@ namespace Microsoft.Maui.Handlers { - + // https://docs.gtk.org/gtk3/class.SpinButton.html public partial class StepperHandler : ViewHandler { @@ -11,13 +11,13 @@ protected override SpinButton CreatePlatformView() { // var adjustment = new Adjustment(0, 0, 1, 1, 1, 1); // return new SpinButton(adjustment, 1, 1); - return new SpinButton(0, 1, .1) { Numeric = true, ClimbRate = 0.1}; + return new SpinButton(0, 1, .1) { Numeric = true, ClimbRate = 0.1 }; } protected override void ConnectHandler(SpinButton nativeView) { base.ConnectHandler(nativeView); - + _ = PlatformView ?? throw new InvalidOperationException($"{nameof(PlatformView)} should have been set by base class."); nativeView.ValueChanged += OnNativeViewValueChanged; @@ -28,7 +28,7 @@ protected override void ConnectHandler(SpinButton nativeView) protected override void DisconnectHandler(SpinButton nativeView) { base.DisconnectHandler(nativeView); - + _ = PlatformView ?? throw new InvalidOperationException($"{nameof(PlatformView)} should have been set by base class."); nativeView.ValueChanged += OnNativeViewValueChanged; @@ -36,7 +36,7 @@ protected override void DisconnectHandler(SpinButton nativeView) void OnNativeViewValueChanged(object? sender, EventArgs e) { - if (sender is not SpinButton nativeView || VirtualView is not { } virtualView) + if (sender is not SpinButton nativeView || VirtualView is not { } virtualView) return; virtualView.Value = nativeView.Value; diff --git a/src/Core/src/Handlers/Switch/SwitchHandler.Gtk.cs b/src/Core/src/Handlers/Switch/SwitchHandler.Gtk.cs index 516628f747b3..d8300f1ac675 100644 --- a/src/Core/src/Handlers/Switch/SwitchHandler.Gtk.cs +++ b/src/Core/src/Handlers/Switch/SwitchHandler.Gtk.cs @@ -2,9 +2,9 @@ namespace Microsoft.Maui.Handlers { - + // https://docs.gtk.org/gtk3/class.Switch.html - + public partial class SwitchHandler : ViewHandler { // diff --git a/src/Core/src/Handlers/View/ViewHandler.Gtk.cs b/src/Core/src/Handlers/View/ViewHandler.Gtk.cs index 93da88832b17..ad20582552bc 100644 --- a/src/Core/src/Handlers/View/ViewHandler.Gtk.cs +++ b/src/Core/src/Handlers/View/ViewHandler.Gtk.cs @@ -36,7 +36,7 @@ public static void MapAnchorX(IViewHandler handler, IView view) { } [MissingMapper] public static void MapAnchorY(IViewHandler handler, IView view) { } - + [MissingMapper] public virtual bool NeedsContainer => false; diff --git a/src/Core/src/LifecycleEvents/Gtk/GtkLifecycle.cs b/src/Core/src/LifecycleEvents/Gtk/GtkLifecycle.cs index 4b7784c3a036..1cd03cab5f48 100644 --- a/src/Core/src/LifecycleEvents/Gtk/GtkLifecycle.cs +++ b/src/Core/src/LifecycleEvents/Gtk/GtkLifecycle.cs @@ -42,7 +42,7 @@ public static class GtkLifecycle public delegate void OnStateChanged(Gtk.Window window, Gtk.WindowStateEventArgs args); public delegate void OnDelete(Gtk.Window window, Gtk.DeleteEventArgs args); - + internal delegate void OnMauiContextCreated(IMauiContext mauiContext); diff --git a/src/Core/src/Platform/Gtk/ApplicationExtensions.cs b/src/Core/src/Platform/Gtk/ApplicationExtensions.cs index 6cb9a7fdc6f7..47f0c8f7642b 100644 --- a/src/Core/src/Platform/Gtk/ApplicationExtensions.cs +++ b/src/Core/src/Platform/Gtk/ApplicationExtensions.cs @@ -16,6 +16,7 @@ public static void CreatePlatformWindow(this Gtk.Application platformApplication var mainWindow = new MauiGtkMainWindow(); platformApplication.AddWindow(mainWindow); + MauiGtkApplication.Current.MainWindow = mainWindow; var mauiContext = applicationContext!.MakeWindowScope(mainWindow, out var windowScope); diff --git a/src/Core/src/Platform/Gtk/ButtonExtensions.cs b/src/Core/src/Platform/Gtk/ButtonExtensions.cs index 3eb48ad90ad4..a9ef6d417b78 100644 --- a/src/Core/src/Platform/Gtk/ButtonExtensions.cs +++ b/src/Core/src/Platform/Gtk/ButtonExtensions.cs @@ -24,7 +24,7 @@ public static void UpdateText(this Button nativeButton, ITextButton button) [MissingMapper] public static void UpdateLineBreakMode(this Button nativeButton, ITextButton button) { - + } } diff --git a/src/Core/src/Platform/Gtk/ContentView.cs b/src/Core/src/Platform/Gtk/ContentView.cs index 1af8361a3309..a5a998f62d4f 100644 --- a/src/Core/src/Platform/Gtk/ContentView.cs +++ b/src/Core/src/Platform/Gtk/ContentView.cs @@ -5,10 +5,10 @@ namespace Microsoft.Maui.Platform { - public class ContentView : Gtk.Box + public class ContentView : Gtk.EventBox { - public ContentView() : base(Orientation.Horizontal, 0) { } + public ContentView() : base() { } internal Func? CrossPlatformMeasure { get; set; } @@ -28,7 +28,7 @@ public Widget? Content } else if (value != null) { - PackStart(value, true, true, 0); + Add(value); } _content = value; diff --git a/src/Core/src/Platform/Gtk/DatePickerExtensions.cs b/src/Core/src/Platform/Gtk/DatePickerExtensions.cs index d65389f44c1e..01d3c9efae55 100644 --- a/src/Core/src/Platform/Gtk/DatePickerExtensions.cs +++ b/src/Core/src/Platform/Gtk/DatePickerExtensions.cs @@ -9,7 +9,7 @@ public static void UpdateDate(this MauiDatePicker nativeDatePicker, IDatePicker nativeDatePicker.Date = datePicker.Date; } - + public static void UpdateFormat(this MauiDatePicker nativeDatePicker, IDatePicker datePicker) { nativeDatePicker.Format = datePicker.Format; diff --git a/src/Core/src/Platform/Gtk/EditorExtensions.cs b/src/Core/src/Platform/Gtk/EditorExtensions.cs index 8760201de1fe..0278e97c58cc 100644 --- a/src/Core/src/Platform/Gtk/EditorExtensions.cs +++ b/src/Core/src/Platform/Gtk/EditorExtensions.cs @@ -11,7 +11,8 @@ public static void UpdateText(this TextView nativeEditor, IEditor editor) var text = editor.Text; var buffer = nativeEditor.Buffer; - if (buffer.Text == text) return; + if (buffer.Text == text) + return; if (text == null) { diff --git a/src/Core/src/Platform/Gtk/GtkCssExtensions.cs b/src/Core/src/Platform/Gtk/GtkCssExtensions.cs index d98d3ec89996..4f2abe4a08d3 100644 --- a/src/Core/src/Platform/Gtk/GtkCssExtensions.cs +++ b/src/Core/src/Platform/Gtk/GtkCssExtensions.cs @@ -21,7 +21,8 @@ public static string CssMainNode(this Gtk.Widget nativeView) case Gtk.ComboBox box: default: - mainNode = nativeView.StyleContext.Path.ToString().Split(':')[0]; + var pathSegments = nativeView.StyleContext.Path.ToString().Split(' '); + mainNode = pathSegments[pathSegments.Length - 1].Split(':')[0]; break; } diff --git a/src/Core/src/Platform/Gtk/IGtkContainer.cs b/src/Core/src/Platform/Gtk/IGtkContainer.cs index 5fb1be812c6e..3050d7fd87ca 100644 --- a/src/Core/src/Platform/Gtk/IGtkContainer.cs +++ b/src/Core/src/Platform/Gtk/IGtkContainer.cs @@ -3,7 +3,7 @@ namespace Microsoft.Maui public interface IGtkContainer { - void ReplaceChild (Gtk.Widget oldWidget, Gtk.Widget newWidget); + void ReplaceChild(Gtk.Widget oldWidget, Gtk.Widget newWidget); } } \ No newline at end of file diff --git a/src/Core/src/Platform/Gtk/ImageView.cs b/src/Core/src/Platform/Gtk/ImageView.cs index bfcf85df4dfb..4adc6aa915fd 100644 --- a/src/Core/src/Platform/Gtk/ImageView.cs +++ b/src/Core/src/Platform/Gtk/ImageView.cs @@ -1,3 +1,5 @@ +using Gdk; + namespace Microsoft.Maui.Platform { @@ -9,11 +11,60 @@ namespace Microsoft.Maui.Platform public class ImageView : Gtk.Image { + // OriginalSizeBuf is added so when we want to resize the image using Pixbuf we can access the + // initial Pixbuf and resize that. + Pixbuf? _originalSizeBuf; + // Image might be null because it is possible to have a WidthRequest before Pixbuf has a value. + // In this case, we'll just save the value, and once Pixbuf is initialized we'll update its size + // to have the saved values + int? _lastRequestedWidth; + int? _lastRequestedHeight; - public Gdk.Pixbuf? Image + public Pixbuf? Image { get => Pixbuf; - set => Pixbuf = value; + set + { + _originalSizeBuf = value; + if (_lastRequestedHeight is not null || _lastRequestedWidth is not null) + { + Pixbuf = _originalSizeBuf?.ScaleSimple( + _lastRequestedWidth ?? _originalSizeBuf.Width, + _lastRequestedHeight ?? _originalSizeBuf.Height, + InterpType.Bilinear + ); + } + else + { + Pixbuf = value; + } + } + } + + internal void ChangeHeight(int height) + { + if (Image is null) + { + _lastRequestedHeight = height; + } + else + { + var newBuf = _originalSizeBuf?.ScaleSimple(Pixbuf.Width, height, InterpType.Bilinear); + Pixbuf = newBuf; + } + } + + internal void ChangeWidth(int width) + { + if (Image is null) + { + _lastRequestedWidth = width; + } + else + { + var newBuf = _originalSizeBuf?.ScaleSimple(width, Pixbuf.Height, InterpType.Bilinear); + Pixbuf = newBuf; + } } } diff --git a/src/Core/src/Platform/Gtk/ImageViewExtensions.cs b/src/Core/src/Platform/Gtk/ImageViewExtensions.cs index b268060a18f5..62317b88c160 100644 --- a/src/Core/src/Platform/Gtk/ImageViewExtensions.cs +++ b/src/Core/src/Platform/Gtk/ImageViewExtensions.cs @@ -78,10 +78,6 @@ public static void UpdateIsAnimationPlaying(this ImageView imageView, IImageSour // no-op } -#pragma warning disable CS0168 - catch (Exception ex) -#pragma warning restore CS0168 - { } finally { // only mark as finished if we are still working on the same image diff --git a/src/Core/src/Platform/Gtk/LabelExtensions.cs b/src/Core/src/Platform/Gtk/LabelExtensions.cs index c98ea18820b3..60d9474ae985 100644 --- a/src/Core/src/Platform/Gtk/LabelExtensions.cs +++ b/src/Core/src/Platform/Gtk/LabelExtensions.cs @@ -34,7 +34,7 @@ public static string HtmlToPangoMarkup(string text) public static void UpdateMaxLines(this Label nativeLabel, ILabel label) { - // nativeLabel.Lines = label.MaxLines; + nativeLabel.Lines = label.MaxLines; nativeLabel.AdjustMaxLines(); } @@ -111,9 +111,9 @@ public static Microsoft.Maui.Graphics.Platform.Gtk.LineBreakMode GetLineBreakMod public static void UpdateLineBreakMode(this Label nativeLabel, ILabel label) { - var labelLineBreakMode = LineBreakMode.CharacterWrap; - var labelMaxLines = 0; - + var labelLineBreakMode = label.LineBreakMode; + var labelMaxLines = label.MaxLines; + switch (labelLineBreakMode) { case LineBreakMode.NoWrap: diff --git a/src/Core/src/Platform/Gtk/LayoutView.cs b/src/Core/src/Platform/Gtk/LayoutView.cs index 2ff10fcd1add..adfb66490bd7 100644 --- a/src/Core/src/Platform/Gtk/LayoutView.cs +++ b/src/Core/src/Platform/Gtk/LayoutView.cs @@ -1,4 +1,4 @@ -#define TRACE_ALLOCATION +#define TRACE_ALLOCATION_ using System; using System.Collections.Concurrent; @@ -79,7 +79,7 @@ Orientation GetOrientation() => var focusChain = _children .Select(c => c.widget) - // .OrderBy(kvp => orientation == Orientation.Horizontal ? kvp.Value.Rect.X : kvp.Value.Rect.Y) + // .OrderBy(kvp => orientation == Orientation.Horizontal ? kvp.Value.Rect.X : kvp.Value.Rect.Y) .ToArray(); FocusChain = focusChain; @@ -294,7 +294,7 @@ public Size Measure(double widthConstraint, double heightConstraint, SizeRequest bool CanBeCached() => !double.IsPositiveInfinity(widthConstraint) && !double.IsPositiveInfinity(heightConstraint); - if (VirtualView is not { } virtualView) + if (VirtualView is not { } virtualView) return Size.Zero; var key = (widthConstraint, heightConstraint, mode); @@ -302,15 +302,19 @@ public Size Measure(double widthConstraint, double heightConstraint, SizeRequest Size cached = Size.Zero; bool cacheHit = CanBeCached() && MeasureCache.TryGetValue(key, out cached); - - if (cacheHit) - { -#if TRACE_ALLOCATION - if (!_checkCacheHitFailed) -#endif - return cached; - - } + // TODO: the code is commented because using cache was causing problems for Layout sizes + // after window looses or gains focus. Ultimately we need to figure out the problem caused + // by caching and stop disabling it to have better performance. + /* + if (cacheHit) + { + #if TRACE_ALLOCATION + if (!_checkCacheHitFailed) + #endif + return cached; + + } + */ var measured = VirtualView.CrossPlatformMeasure(widthConstraint, heightConstraint); @@ -368,7 +372,7 @@ protected override void OnAdjustSizeRequest(Orientation orientation, out int min { if (RequestMode is SizeRequestMode.WidthForHeight or SizeRequestMode.ConstantSize) { - if (MeasuredSizeV is { Width : > 0 } size && (constraint == 0)) + if (MeasuredSizeV is { Width: > 0 } size && (constraint == 0)) constraint = size.Width; constraint = constraint == 0 ? double.PositiveInfinity : constraint; @@ -427,7 +431,8 @@ public void Arrange(Rectangle rect) if (rect.IsEmpty) return; - if (rect == Allocation.ToRect()) return; + if (rect == Allocation.ToRect()) + return; if (IsSizeAllocating) { diff --git a/src/Core/src/Platform/Gtk/MauiDatePicker.cs b/src/Core/src/Platform/Gtk/MauiDatePicker.cs index d4b185ca7cfe..196b2ee18a4d 100644 --- a/src/Core/src/Platform/Gtk/MauiDatePicker.cs +++ b/src/Core/src/Platform/Gtk/MauiDatePicker.cs @@ -1,35 +1,416 @@ using System; using System.Runtime.Serialization; +using System.Linq; +using Gtk; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Graphics.Platform.Gtk; namespace Microsoft.Maui.Platform { - public class MauiDatePicker : Gtk.Label + public class DateEventArgs : EventArgs { + DateTime _date; - public MauiDatePicker() : base() + public DateTime Date { - Format = string.Empty; + get + { + return _date; + } + } + + public DateEventArgs(DateTime date) + { + _date = date; + } + } + + public class GrabHelper + { + public static void GrabWindow(Window window) + { + window.GrabFocus(); + + Grab.Add(window); + Gdk.GrabStatus grabbed = + Gdk.Display.Default.DefaultSeat.Grab(window.Window, Gdk.SeatCapabilities.All, true, null, null, null); + if (grabbed != Gdk.GrabStatus.Success) + { + Grab.Remove(window); + window.Destroy(); + } + } + + public static void RemoveGrab(Window window) + { + Grab.Remove(window); + Gdk.Display.Default.DefaultSeat.Ungrab(); + } + } + + public partial class DatePickerWindow : Window + { + Box _datebox; + RangeCalendar _calendar; + public delegate void DateEventHandler(object sender, DateEventArgs args); + + public event DateEventHandler? OnDateTimeChanged; + + public DatePickerWindow(DateTime initialDate, Window? parentWindow) + : base(WindowType.Popup) + { + if (parentWindow is not null) + TransientFor = parentWindow; + Title = "DatePicker"; + TypeHint = Gdk.WindowTypeHint.Desktop; + WindowPosition = WindowPosition.None; + BorderWidth = 1; + Resizable = false; + Decorated = false; + DestroyWithParent = true; + SkipPagerHint = true; + SkipTaskbarHint = true; + _datebox = new Box(Orientation.Vertical, 6); + _datebox.BorderWidth = 3; + + _calendar = new RangeCalendar(); + _calendar.CanFocus = true; + _calendar.DisplayOptions = CalendarDisplayOptions.ShowHeading; + _datebox.Add(_calendar); + Box.BoxChild dateBoxChild = ((Box.BoxChild)(_datebox[_calendar])); + dateBoxChild.Position = 0; + + Add(_datebox); + + if ((Child != null)) + { + Child.ShowAll(); + } + + SelectedDate = initialDate; + Show(); + ButtonPressEvent += new ButtonPressEventHandler(OnButtonPressEvent); + _calendar.ButtonPressEvent += new ButtonPressEventHandler(OnCalendarButtonPressEvent); + _calendar.DaySelected += new EventHandler(OnCalendarDaySelected); + _calendar.DaySelectedDoubleClick += new EventHandler(OnCalendarDaySelectedDoubleClick); + GrabHelper.GrabWindow(this); + } + + public DateTime SelectedDate + { + get + { + return _calendar.Date; + } + + set + { + _calendar.Date = value; + } + } + + public DateTime MinimumDate + { + get + { + return _calendar.MinimumDate; + } + + set + { + _calendar.MinimumDate = value; + } + } + + public DateTime MaximumDate + { + get + { + return _calendar.MaximumDate; + } + + set + { + _calendar.MaximumDate = value; + } + } + + protected virtual void OnButtonPressEvent(object sender, ButtonPressEventArgs args) + { + CloseCalendar(); + } + + protected virtual void OnCalendarButtonPressEvent(object sender, ButtonPressEventArgs args) + { + args.RetVal = true; + } + + protected virtual void OnCalendarDaySelected(object? sender, EventArgs eventArgs) + { + OnDateTimeChanged?.Invoke(this, new DateEventArgs(SelectedDate)); + } + + protected virtual void OnCalendarDaySelectedDoubleClick(object? sender, EventArgs eventArgs) + { + OnDateTimeChanged?.Invoke(this, new DateEventArgs(SelectedDate)); + CloseCalendar(); + } + + public void ShowCalendar() + { + if ((Child != null)) + { + Child.ShowAll(); + } + + Show(); + GrabHelper.GrabWindow(this); + } + + void CloseCalendar() + { + GrabHelper.RemoveGrab(this); + Hide(); + } + + void NotifyDateChanged() + { + OnDateTimeChanged?.Invoke(this, new DateEventArgs(SelectedDate)); + } + + class RangeCalendar : Calendar + { + DateTime _minimumDate; + DateTime _maximumDate; + + public RangeCalendar() + { + _minimumDate = new DateTime(1900, 1, 1); + _maximumDate = new DateTime(2199, 1, 1); + } + + public DateTime MinimumDate + { + get + { + return _minimumDate; + } + + set + { + if (MaximumDate < value) + { + throw new InvalidOperationException($"{nameof(MinimumDate)} must be lower than {nameof(MaximumDate)}"); + } + + _minimumDate = value; + } + } + + public DateTime MaximumDate + { + get + { + return _maximumDate; + } + + set + { + if (MinimumDate > value) + { + throw new InvalidOperationException($"{nameof(MaximumDate)} must be greater than {nameof(MinimumDate)}"); + } + + _maximumDate = value; + } + } + + protected override void OnDaySelected() + { + if (Date < MinimumDate) + { + Date = MinimumDate; + } + else if (Date > MaximumDate) + { + Date = MaximumDate; + } + } } + } -#pragma warning disable 169 - // to simulate - // Tapping either of the DatePicker displays invokes the platform date picker - Gtk.Calendar? _calendar; -#pragma warning restore 169 - - DateTime _time; + public class CustomComboBox : Gtk.Box + { + private Gtk.Entry _entry; + private Color _color = Colors.Black; + + public CustomComboBox() : base(Orientation.Horizontal, 0) + { + _entry = new Gtk.Entry(); + _entry.CanFocus = true; + _entry.IsEditable = true; + _entry.SetIconFromIconName(Gtk.EntryIconPosition.Secondary, "pan-down-symbolic"); + PackStart(_entry, true, true, 0); + } + + public Gtk.Entry Entry + { + get + { + return _entry; + } + } + + public Color Color + { + get { return _color; } + set + { + _color = value; + Entry.UpdateTextColor(_color); + } + } + } + + public class MauiDatePicker : EventBox + { + readonly CustomComboBox _comboBox; + DateTime _currentDate; + string? _dateFormat; + DatePickerWindow? _pickerWindow; + + public event EventHandler DateChanged = new EventHandler((sender, args) => { }); + public event EventHandler GotFocus = new EventHandler((sender, args) => { }); + public event EventHandler LostFocus = new EventHandler((sender, args) => { }); + + public MauiDatePicker() + { + _comboBox = new CustomComboBox(); + Add(_comboBox); + + if ((Child != null)) + Child.ShowAll(); + Show(); + + Date = DateTime.Now; + Format = string.Empty; + MinDate = new DateTime(1900, 1, 1); + MaxDate = new DateTime(2199, 1, 1); + + _comboBox.Entry.CanDefault = false; + _comboBox.Entry.CanFocus = false; + _comboBox.Entry.IsEditable = false; + _comboBox.Entry.SetStateFlags(StateFlags.Normal, true); + _comboBox.Entry.CanFocus = false; + _comboBox.Entry.IsEditable = false; + _comboBox.Entry.FocusGrabbed += new EventHandler(OnEntryFocused); + _comboBox.Entry.IconPress += new Gtk.IconPressHandler(OnBtnShowCalendarClicked); + } public DateTime Date { - get => _time; + get + { + return _currentDate; + } + set + { + _currentDate = value; + UpdateEntryText(); + } + } + + public DateTime MinDate { get; set; } + + public DateTime MaxDate { get; set; } + + public string? Format + { + get + { + return _dateFormat; + } set { - _time = value; - Text = _time.ToString(); + _dateFormat = value; + UpdateEntryText(); } } - public string Format { get; set; } + void ShowPickerWindow() + { + int x = 0; + int y = 0; + + Window.GetOrigin(out x, out y); + y += Allocation.Height; + if (_pickerWindow is null) + { + var picker = new DatePickerWindow(Date, this.Toplevel as Window); + picker.Move(x, y); + picker.MinimumDate = MinDate; + picker.MaximumDate = MaxDate; + picker.OnDateTimeChanged += OnPopupDateChanged; + picker.Destroyed += OnPickerClosed; + _pickerWindow = picker; + } + else + { + if (_pickerWindow.SelectedDate != Date) + _pickerWindow.SelectedDate = Date; + _pickerWindow.MinimumDate = MinDate; + _pickerWindow.MaximumDate = MaxDate; + _pickerWindow.ShowCalendar(); + _pickerWindow.Move(x, y); + } + } + + void OnPopupDateChanged(object sender, DateEventArgs args) + { + var date = args.Date; + + if (date < MinDate) + { + Date = MinDate; + return; + } + + if (date > MaxDate) + { + Date = MaxDate; + return; + } + + Date = args.Date; + DateChanged?.Invoke(this, EventArgs.Empty); + } + + void UpdateEntryText() + { + _comboBox.Entry.Text = _currentDate.ToString(string.IsNullOrEmpty(_dateFormat) ? "D" : _dateFormat); + } + + void OnBtnShowCalendarClicked(object? sender, EventArgs eventArgs) + { + ShowPickerWindow(); + } + + void OnEntryFocused(object? sender, EventArgs eventArgs) + { + ShowPickerWindow(); + GotFocus?.Invoke(this, EventArgs.Empty); + } + + void OnPickerClosed(object? sender, EventArgs eventArgs) + { + var window = sender as DatePickerWindow; + + if (window != null) + { + window.Hide(); + LostFocus?.Invoke(this, EventArgs.Empty); + } + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Gtk/MauiGtkApplication.cs b/src/Core/src/Platform/Gtk/MauiGtkApplication.cs index f0f80331e96c..af9e3275e6e9 100644 --- a/src/Core/src/Platform/Gtk/MauiGtkApplication.cs +++ b/src/Core/src/Platform/Gtk/MauiGtkApplication.cs @@ -29,7 +29,7 @@ public string? Name public static MauiGtkApplication Current { get; internal set; } = null!; - public MauiGtkMainWindow MainWindow { get; protected set; } = null!; + public MauiGtkMainWindow MainWindow { get; set; } = null!; // set is not protected because it needs to be accessed from ApplicationExtensions.cs public IServiceProvider Services { get; protected set; } = null!; diff --git a/src/Core/src/Platform/Gtk/MauiSearchBar.cs b/src/Core/src/Platform/Gtk/MauiSearchBar.cs index df1128edda39..b5a902cd1a4d 100644 --- a/src/Core/src/Platform/Gtk/MauiSearchBar.cs +++ b/src/Core/src/Platform/Gtk/MauiSearchBar.cs @@ -9,7 +9,7 @@ public class MauiSearchBar : Gtk.SearchBar public MauiSearchBar() : base() { - Entry = new (string.Empty); + Entry = new(string.Empty); Child = Entry; SearchModeEnabled = true; } diff --git a/src/Core/src/Platform/Gtk/NavigationView.cs b/src/Core/src/Platform/Gtk/NavigationView.cs index 1f4792297d41..81bf561098c5 100644 --- a/src/Core/src/Platform/Gtk/NavigationView.cs +++ b/src/Core/src/Platform/Gtk/NavigationView.cs @@ -1,11 +1,44 @@ +using System; +using System.Linq; +using Gtk; + namespace Microsoft.Maui.Platform { public class NavigationView : Gtk.Box { + Gtk.Widget? pageWidget = null; + IMauiContext? mauiContext = null; public NavigationView() : base(Gtk.Orientation.Horizontal, 0) { } + public void Connect(IStackNavigationView virtualView) + { + mauiContext = virtualView.Handler?.MauiContext; + } + + public void Disconnect(IStackNavigationView virtualView) + { + } + + public void RequestNavigation(NavigationRequest request) + { + // stack top is last + var page = request.NavigationStack.Last(); + var newPageWidget = page.ToPlatform(mauiContext!); + if (pageWidget is null) + { + this.PackStart(newPageWidget, true, true, 0); + } + else + { + this.Remove(pageWidget); + this.Add(newPageWidget); + this.SetChildPacking(newPageWidget, true, true, 0, PackType.Start); + } + pageWidget = newPageWidget; + } + } } \ No newline at end of file diff --git a/src/Core/src/Platform/Gtk/NotImplementedView.cs b/src/Core/src/Platform/Gtk/NotImplementedView.cs index 74173d0179d0..020982108a7b 100644 --- a/src/Core/src/Platform/Gtk/NotImplementedView.cs +++ b/src/Core/src/Platform/Gtk/NotImplementedView.cs @@ -1,9 +1,9 @@ using System; namespace Microsoft.Maui.Platform { -/// -/// dummy widget to mark handler as not implemented -/// but avoid -/// + /// + /// dummy widget to mark handler as not implemented + /// but avoid + /// public class NotImplementedView : Gtk.Widget { } } \ No newline at end of file diff --git a/src/Core/src/Platform/Gtk/SearchBarExtensions.cs b/src/Core/src/Platform/Gtk/SearchBarExtensions.cs index 1927a2ed4d89..ec6d46cd3aa3 100644 --- a/src/Core/src/Platform/Gtk/SearchBarExtensions.cs +++ b/src/Core/src/Platform/Gtk/SearchBarExtensions.cs @@ -2,6 +2,6 @@ { public static class SearchBarExtensions { - + } } diff --git a/src/Core/src/Platform/Gtk/StepperExtensions.cs b/src/Core/src/Platform/Gtk/StepperExtensions.cs index a047556b7f6f..0837e03bfad7 100644 --- a/src/Core/src/Platform/Gtk/StepperExtensions.cs +++ b/src/Core/src/Platform/Gtk/StepperExtensions.cs @@ -18,7 +18,7 @@ public static void UpdateValue(this SpinButton nativeSlider, IRange slider) } public static void UpdateIncrement(this SpinButton nativeSlider, IStepper slider) { - nativeSlider.SetIncrements(slider.Interval,1); + nativeSlider.SetIncrements(slider.Interval, 1); } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Gtk/TextAlignmentExtensions.cs b/src/Core/src/Platform/Gtk/TextAlignmentExtensions.cs index 38150a580003..2aaa6a5d8bf0 100644 --- a/src/Core/src/Platform/Gtk/TextAlignmentExtensions.cs +++ b/src/Core/src/Platform/Gtk/TextAlignmentExtensions.cs @@ -4,10 +4,10 @@ namespace Microsoft.Maui { public static class TextAlignmentExtensions { - + // https://docs.gtk.org/gtk3/property.Widget.halign // How to distribute horizontal space if widget gets extra space, see GtkAlign - + internal static Align ToGtkAlign(this TextAlignment alignment) { switch (alignment) @@ -20,7 +20,7 @@ internal static Align ToGtkAlign(this TextAlignment alignment) return Align.Center; } } - + public static Justification ToJustification(this TextAlignment alignment) { switch (alignment) @@ -33,7 +33,7 @@ public static Justification ToJustification(this TextAlignment alignment) return Justification.Center; } } - + /// /// https://docs.gtk.org/gtk3/method.Label.set_xalign.html /// The xalign property determines the horizontal aligment of the label text inside the labels size allocation. diff --git a/src/Core/src/Platform/Gtk/ThicknessExtensions.cs b/src/Core/src/Platform/Gtk/ThicknessExtensions.cs index 538e62c7481b..ddfe47a51ccb 100644 --- a/src/Core/src/Platform/Gtk/ThicknessExtensions.cs +++ b/src/Core/src/Platform/Gtk/ThicknessExtensions.cs @@ -28,7 +28,7 @@ public static Border ToNative(this Thickness it) public static Thickness GetPadding(this TWidget? it) where TWidget : Widget => it == default ? default : new Thickness(it.MarginStart, it.MarginTop, it.MarginEnd, it.MarginBottom); - + public static Thickness GetPaddingThickness(this TWidget? it) where TWidget : Widget { diff --git a/src/Core/src/Platform/Gtk/TimePickerExtensions.cs b/src/Core/src/Platform/Gtk/TimePickerExtensions.cs index eb0bbf44f443..fa38bbcd1260 100644 --- a/src/Core/src/Platform/Gtk/TimePickerExtensions.cs +++ b/src/Core/src/Platform/Gtk/TimePickerExtensions.cs @@ -9,7 +9,7 @@ public static void UpdateTime(this MauiTimePicker nativeTimePicker, ITimePicker nativeTimePicker.Time = timePicker.Time; } - + public static void UpdateFormat(this MauiTimePicker nativeTimePicker, ITimePicker timePicker) { nativeTimePicker.Format = timePicker.Format; diff --git a/src/Core/src/Platform/Gtk/WidgetColorExtensions.cs b/src/Core/src/Platform/Gtk/WidgetColorExtensions.cs index ef4e863d884e..3c280fdeb1a1 100644 --- a/src/Core/src/Platform/Gtk/WidgetColorExtensions.cs +++ b/src/Core/src/Platform/Gtk/WidgetColorExtensions.cs @@ -32,8 +32,9 @@ public static void SetBackgroundColor(this Gtk.Widget widget, Gtk.StateFlags sta var cssFlags = state.CssState(); var mainNode = widget.CssMainNode(); - if (cssFlags != null) mainNode = $"{mainNode}:{cssFlags}"; - widget.SetStyleColor(color, mainNode, "background-color"); + if (cssFlags != null) + mainNode = $"{mainNode}:{cssFlags}"; + widget.SetStyleColor(color, mainNode, "background"); } diff --git a/src/Core/src/Platform/Gtk/WidgetExtensions.cs b/src/Core/src/Platform/Gtk/WidgetExtensions.cs index da03d12d4e0f..eda6b8694f63 100644 --- a/src/Core/src/Platform/Gtk/WidgetExtensions.cs +++ b/src/Core/src/Platform/Gtk/WidgetExtensions.cs @@ -25,6 +25,7 @@ public static void UpdateVisibility(this Widget nativeView, Visibility visibilit break; case Visibility.Collapsed: + nativeView.Visible = false; break; default: @@ -169,47 +170,47 @@ public static void ReplaceChild(this Gtk.Container cont, Gtk.Widget oldWidget, G break; case Gtk.Notebook notebook: - { - Gtk.Notebook.NotebookChild nc = (Gtk.Notebook.NotebookChild)notebook[oldWidget]; - var detachable = nc.Detachable; - var pos = nc.Position; - var reorderable = nc.Reorderable; - var tabExpand = nc.TabExpand; - var tabFill = nc.TabFill; - var label = notebook.GetTabLabel(oldWidget); - notebook.Remove(oldWidget); - notebook.InsertPage(newWidget, label, pos); - - nc = (Gtk.Notebook.NotebookChild)notebook[newWidget]; - nc.Detachable = detachable; - nc.Reorderable = reorderable; - nc.TabExpand = tabExpand; - nc.TabFill = tabFill; - - break; - } - case Gtk.Paned paned: - { - var pc = (Gtk.Paned.PanedChild)paned[oldWidget]; - var resize = pc.Resize; - var shrink = pc.Shrink; - var pos = paned.Position; - - if (paned.Child1 == oldWidget) { - paned.Remove(oldWidget); - paned.Pack1(newWidget, resize, shrink); + Gtk.Notebook.NotebookChild nc = (Gtk.Notebook.NotebookChild)notebook[oldWidget]; + var detachable = nc.Detachable; + var pos = nc.Position; + var reorderable = nc.Reorderable; + var tabExpand = nc.TabExpand; + var tabFill = nc.TabFill; + var label = notebook.GetTabLabel(oldWidget); + notebook.Remove(oldWidget); + notebook.InsertPage(newWidget, label, pos); + + nc = (Gtk.Notebook.NotebookChild)notebook[newWidget]; + nc.Detachable = detachable; + nc.Reorderable = reorderable; + nc.TabExpand = tabExpand; + nc.TabFill = tabFill; + + break; } - else + case Gtk.Paned paned: { - paned.Remove(oldWidget); - paned.Pack2(newWidget, resize, shrink); + var pc = (Gtk.Paned.PanedChild)paned[oldWidget]; + var resize = pc.Resize; + var shrink = pc.Shrink; + var pos = paned.Position; + + if (paned.Child1 == oldWidget) + { + paned.Remove(oldWidget); + paned.Pack1(newWidget, resize, shrink); + } + else + { + paned.Remove(oldWidget); + paned.Pack2(newWidget, resize, shrink); + } + + paned.Position = pos; + + break; } - - paned.Position = pos; - - break; - } case Gtk.Bin bin: bin.Remove(oldWidget); bin.Child = newWidget; diff --git a/src/Core/src/Platform/Gtk/[Obsolete]/MauiContext.Gtk.cs b/src/Core/src/Platform/Gtk/[Obsolete]/MauiContext.Gtk.cs index 49686cba7296..2e34d8a2a539 100644 --- a/src/Core/src/Platform/Gtk/[Obsolete]/MauiContext.Gtk.cs +++ b/src/Core/src/Platform/Gtk/[Obsolete]/MauiContext.Gtk.cs @@ -4,7 +4,8 @@ namespace Microsoft.Maui { - public partial class MauiContext_ { + public partial class MauiContext_ + { public Gtk.Window? Window { get; internal set; } diff --git a/src/Essentials/src/Accelerometer/Accelerometer.Gtk.cs b/src/Essentials/src/Accelerometer/Accelerometer.Gtk.cs index ee4a81592fd6..72912559c10b 100644 --- a/src/Essentials/src/Accelerometer/Accelerometer.Gtk.cs +++ b/src/Essentials/src/Accelerometer/Accelerometer.Gtk.cs @@ -6,7 +6,7 @@ namespace Microsoft.Maui.Devices.Sensors partial class AccelerometerImplementation { public bool IsSupported => false; - + void PlatformStart(SensorSpeed sensorSpeed) => throw ExceptionUtils.NotSupportedOrImplementedException; diff --git a/src/Essentials/src/Connectivity/Connectivity.Gtk.cs b/src/Essentials/src/Connectivity/Connectivity.Gtk.cs index b262827a64be..3193952655bc 100644 --- a/src/Essentials/src/Connectivity/Connectivity.Gtk.cs +++ b/src/Essentials/src/Connectivity/Connectivity.Gtk.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; + using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Networking @@ -6,16 +10,92 @@ namespace Microsoft.Maui.Networking /// partial class ConnectivityImplementation : IConnectivity { - public NetworkAccess NetworkAccess => - throw ExceptionUtils.NotSupportedOrImplementedException; + void StartListeners() + { + NetworkChange.NetworkAddressChanged += NetworkStatusChanged; + NetworkChange.NetworkAvailabilityChanged += NetworkStatusChanged; + } + + void NetworkStatusChanged(object sender, EventArgs e) => OnConnectivityChanged(); + + void StopListeners() + { + NetworkChange.NetworkAddressChanged -= NetworkStatusChanged; + NetworkChange.NetworkAvailabilityChanged -= NetworkStatusChanged; + } + + public NetworkAccess NetworkAccess + { + get + { + if (!NetworkInterface.GetIsNetworkAvailable()) + return NetworkAccess.None; + + var allLoopback = NetworkInterface.GetAllNetworkInterfaces() + .All(ni => ni.NetworkInterfaceType == NetworkInterfaceType.Loopback); + if (allLoopback) + return NetworkAccess.None; + + var gateways = NetworkInterface.GetAllNetworkInterfaces() + .Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .SelectMany(ni => ni.GetIPProperties().GatewayAddresses); + + if (gateways.Any()) + return NetworkAccess.Internet; + + return NetworkAccess.None; + } + } + + ConnectionProfile ToConnectionType(NetworkInterface networkInterface) + { + switch (networkInterface.NetworkInterfaceType) + { + case NetworkInterfaceType.TokenRing: + case NetworkInterfaceType.Ethernet: + case NetworkInterfaceType.Ethernet3Megabit: + case NetworkInterfaceType.IPOverAtm: + case NetworkInterfaceType.FastEthernetT: + case NetworkInterfaceType.GigabitEthernet: + case NetworkInterfaceType.FastEthernetFx: + case NetworkInterfaceType.GenericModem: + return ConnectionProfile.Ethernet; + + case NetworkInterfaceType.Wireless80211: + case NetworkInterfaceType.Wman: + return ConnectionProfile.WiFi; + + case NetworkInterfaceType.Wwanpp2: + case NetworkInterfaceType.Wwanpp: + return ConnectionProfile.Cellular; + + case NetworkInterfaceType.Ppp: + case NetworkInterfaceType.Fddi: + case NetworkInterfaceType.Isdn: + case NetworkInterfaceType.BasicIsdn: + case NetworkInterfaceType.PrimaryIsdn: + case NetworkInterfaceType.Tunnel: + case NetworkInterfaceType.VeryHighSpeedDsl: + case NetworkInterfaceType.AsymmetricDsl: + case NetworkInterfaceType.RateAdaptDsl: + case NetworkInterfaceType.SymmetricDsl: + case NetworkInterfaceType.Loopback: + case NetworkInterfaceType.Slip: + case NetworkInterfaceType.Atm: + case NetworkInterfaceType.MultiRateSymmetricDsl: + case NetworkInterfaceType.HighPerformanceSerialBus: + case NetworkInterfaceType.Unknown: + return ConnectionProfile.Unknown; - public IEnumerable ConnectionProfiles => - throw ExceptionUtils.NotSupportedOrImplementedException; + default: + return ConnectionProfile.Unknown; + } + } - void StartListeners() => - throw ExceptionUtils.NotSupportedOrImplementedException; + public IEnumerable ConnectionProfiles + { + get => NetworkInterface.GetAllNetworkInterfaces().Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Loopback).Select(ToConnectionType); + } - void StopListeners() => - throw ExceptionUtils.NotSupportedOrImplementedException; } } \ No newline at end of file diff --git a/src/Essentials/src/Launcher/Launcher.Gtk.cs b/src/Essentials/src/Launcher/Launcher.Gtk.cs index 28ecff67906a..803b5e632cc5 100644 --- a/src/Essentials/src/Launcher/Launcher.Gtk.cs +++ b/src/Essentials/src/Launcher/Launcher.Gtk.cs @@ -6,16 +6,24 @@ namespace Microsoft.Maui.ApplicationModel /// partial class LauncherImplementation { - Task PlatformCanOpenAsync(Uri uri) => - throw ExceptionUtils.NotSupportedOrImplementedException; + Task PlatformCanOpenAsync(Uri uri) => Task.FromResult(true); - Task PlatformOpenAsync(Uri uri) => - throw ExceptionUtils.NotSupportedOrImplementedException; + Task PlatformOpenAsync(Uri uri) => GTKTryOpenAsync(uri); Task PlatformOpenAsync(OpenFileRequest request) => throw ExceptionUtils.NotSupportedOrImplementedException; - Task PlatformTryOpenAsync(Uri uri) => - throw ExceptionUtils.NotSupportedOrImplementedException; + Task PlatformTryOpenAsync(Uri uri) => GTKTryOpenAsync(uri); + + // ported from https://github.com/nblockchain/DotNetEssentials/blob/master/Xamarin.Essentials/Launcher/Launcher.netstandard.cs + static async Task GTKTryOpenAsync(Uri uri) + { + string stdout, stderr; + int exitCode; + var task = Task.Run( + () => GLib.Process.SpawnCommandLineSync("xdg-open " + uri.ToString(), out stdout, out stderr, out exitCode)); + var result = await task; + return result; + } } } \ No newline at end of file diff --git a/src/Essentials/src/Platform/Platform.Gtk.cs b/src/Essentials/src/Platform/Platform.Gtk.cs index afc81c6cb475..e8fe569c15ab 100644 --- a/src/Essentials/src/Platform/Platform.Gtk.cs +++ b/src/Essentials/src/Platform/Platform.Gtk.cs @@ -1,7 +1,7 @@ namespace Microsoft.Maui.ApplicationModel { static class PlatformUtils - { - } + { + } } diff --git a/src/Graphics/samples/GraphicsTester.Gtk/MainWindow.cs b/src/Graphics/samples/GraphicsTester.Gtk/MainWindow.cs index 4ed5163c3e01..5ffbb4cfb087 100644 --- a/src/Graphics/samples/GraphicsTester.Gtk/MainWindow.cs +++ b/src/Graphics/samples/GraphicsTester.Gtk/MainWindow.cs @@ -60,11 +60,13 @@ public MainWindow() : base(WindowType.Toplevel) private void Selection_Changed(object sender, EventArgs e) { - if (!_treeView.Selection.GetSelected(out TreeIter iter)) return; + if (!_treeView.Selection.GetSelected(out TreeIter iter)) + return; var s = _store.GetValue(iter, 0).ToString(); - if (!_items.TryGetValue(s, out var scenario)) return; + if (!_items.TryGetValue(s, out var scenario)) + return; _gtkGtkGraphicsView.Drawable = scenario; _gtkGtkGraphicsView.HeightRequest = (int)scenario.Height; diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/CanvasExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/CanvasExtensions.cs index 1621e87c455e..6f7549294ce2 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/CanvasExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/CanvasExtensions.cs @@ -1,22 +1,26 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class CanvasExtensions { +public static class CanvasExtensions +{ public static Cairo.LineJoin ToLineJoin(this LineJoin lineJoin) => - lineJoin switch { + lineJoin switch + { LineJoin.Bevel => Cairo.LineJoin.Bevel, LineJoin.Round => Cairo.LineJoin.Round, _ => Cairo.LineJoin.Miter }; public static Cairo.FillRule ToFillRule(this WindingMode windingMode) => - windingMode switch { + windingMode switch + { WindingMode.EvenOdd => Cairo.FillRule.EvenOdd, _ => Cairo.FillRule.Winding }; public static Cairo.LineCap ToLineCap(this LineCap lineCap) => - lineCap switch { + lineCap switch + { LineCap.Butt => Cairo.LineCap.Butt, LineCap.Round => Cairo.LineCap.Round, _ => Cairo.LineCap.Square @@ -24,7 +28,8 @@ public static Cairo.LineCap ToLineCap(this LineCap lineCap) => public static Cairo.Antialias ToAntialias(bool antialias) => antialias ? Cairo.Antialias.Default : Cairo.Antialias.None; - public static Size? GetSize(this Cairo.Surface it) { + public static Size? GetSize(this Cairo.Surface it) + { if (it is Cairo.ImageSurface i) return new Size(i.Width, i.Height); diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/ColorExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/ColorExtensions.cs index 4fcc5506d9a7..392a791dd1cb 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/ColorExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/ColorExtensions.cs @@ -2,13 +2,26 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class ColorExtensions { +public static class ColorExtensions +{ public static Gdk.RGBA ToGdkRgba(this Color color) - => color == default ? default : new Gdk.RGBA {Red = color.Red, Green = color.Green, Blue = color.Blue, Alpha = color.Alpha}; + => color == default ? default : new Gdk.RGBA { Red = color.Red, Green = color.Green, Blue = color.Blue, Alpha = color.Alpha }; public static Color ToColor(this Gdk.RGBA color) - => new Color((float) color.Red, (float) color.Green, (float) color.Blue, (float) color.Alpha); + => new Color((float)color.Red, (float)color.Green, (float)color.Blue, (float)color.Alpha); + + public static Gdk.Color ToGdkColor(this Color color) + { + string hex = color.ToRgbaHex(); + Gdk.Color gtkColor = new Gdk.Color(); + // error CS0612: 'Color.Parse(string, ref Color)' is obsolete +#pragma warning disable 612 + Gdk.Color.Parse(hex, ref gtkColor); +#pragma warning restore 612 + + return gtkColor; + } public static Cairo.Color ToCairoColor(this Color color) => color == default ? default : new Cairo.Color(color.Red, color.Green, color.Blue, color.Alpha); @@ -17,6 +30,6 @@ public static Cairo.Color ToCairoColor(this Gdk.RGBA color) => new Cairo.Color(color.Red, color.Green, color.Blue, color.Alpha); public static Color ToColor(this Cairo.Color color) - => new Color((float) color.R, (float) color.G, (float) color.B, (float) color.A); + => new Color((float)color.R, (float)color.G, (float)color.B, (float)color.A); } \ No newline at end of file diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/FontExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/FontExtensions.cs index 96ae6473b6b6..3f2721720e67 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/FontExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/FontExtensions.cs @@ -62,7 +62,7 @@ public static double ToFontWeight(this Pango.Weight it) => (int)it; public static Pango.FontDescription ToFontDescription(this IFont it) - => it is not {} || string.IsNullOrEmpty(it?.Name) + => it is not { } || string.IsNullOrEmpty(it?.Name) ? SystemFontDescription : new Pango.FontDescription { diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/GraphicsExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/GraphicsExtensions.cs index e859336c1308..472d02baa1ad 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/GraphicsExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/GraphicsExtensions.cs @@ -2,7 +2,8 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class GraphicsExtensions { +public static class GraphicsExtensions +{ public static Rect ToRect(this Gdk.Rectangle it) => new Rect(it.X, it.Y, it.Width, it.Height); @@ -11,10 +12,10 @@ public static RectF ToRectF(this Gdk.Rectangle it) => new RectF(it.X, it.Y, it.Width, it.Height); public static Gdk.Rectangle ToNative(this Rect it) - => new Gdk.Rectangle((int) it.X, (int) it.Y, (int) it.Width, (int) it.Height); + => new Gdk.Rectangle((int)it.X, (int)it.Y, (int)it.Width, (int)it.Height); public static Gdk.Rectangle ToNative(this RectF it) - => new Gdk.Rectangle((int) it.X, (int) it.Y, (int) it.Width, (int) it.Height); + => new Gdk.Rectangle((int)it.X, (int)it.Y, (int)it.Width, (int)it.Height); public static Point ToPoint(this Gdk.Point it) => new Point(it.X, it.Y); @@ -23,13 +24,13 @@ public static PointF ToPointF(this Gdk.Point it) => new PointF(it.X, it.Y); public static PointF ToPointF(this Cairo.PointD it) - => new PointF((float) it.X, (float) it.Y); + => new PointF((float)it.X, (float)it.Y); public static Gdk.Point ToNative(this Point it) - => new Gdk.Point((int) it.X, (int) it.Y); + => new Gdk.Point((int)it.X, (int)it.Y); public static Gdk.Point ToNative(this PointF it) - => new Gdk.Point((int) it.X, (int) it.Y); + => new Gdk.Point((int)it.X, (int)it.Y); public static Size ToSize(this Gdk.Size it) => new Size(it.Width, it.Height); @@ -38,24 +39,24 @@ public static SizeF ToSizeF(this Gdk.Size it) => new SizeF(it.Width, it.Height); public static Gdk.Size ToNative(this Size it) - => new Gdk.Size((int) it.Width, (int) it.Height); + => new Gdk.Size((int)it.Width, (int)it.Height); public static Gdk.Size ToNative(this SizeF it) - => new Gdk.Size((int) it.Width, (int) it.Height); + => new Gdk.Size((int)it.Width, (int)it.Height); public static double ScaledFromPango(this int it) => Math.Ceiling(it / Pango.Scale.PangoScale); public static float ScaledFromPangoF(this int it) - => (float) Math.Ceiling(it / Pango.Scale.PangoScale); + => (float)Math.Ceiling(it / Pango.Scale.PangoScale); public static int ScaledToPango(this double it) - => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + => (int)Math.Ceiling(it * Pango.Scale.PangoScale); public static int ScaledToPango(this float it) - => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + => (int)Math.Ceiling(it * Pango.Scale.PangoScale); public static int ScaledToPango(this int it) - => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + => (int)Math.Ceiling(it * Pango.Scale.PangoScale); } \ No newline at end of file diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/GtkBitmapExportContext.cs b/src/Graphics/src/Graphics.Gtk/Gtk/GtkBitmapExportContext.cs index 0d12f766dea1..5eb4128e7f07 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/GtkBitmapExportContext.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/GtkBitmapExportContext.cs @@ -3,18 +3,21 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public class GtkBitmapExportContext : BitmapExportContext { +public class GtkBitmapExportContext : BitmapExportContext +{ private PlatformCanvas _canvas; private Cairo.ImageSurface _surface; private Cairo.Context _context; private Gdk.Pixbuf? _pixbuf; - public GtkBitmapExportContext(int width, int height, float dpi) : base(width, height, dpi) { + public GtkBitmapExportContext(int width, int height, float dpi) : base(width, height, dpi) + { _surface = new Cairo.ImageSurface(Cairo.Format.Argb32, width, height); _context = new Cairo.Context(_surface); - _canvas = new PlatformCanvas { + _canvas = new PlatformCanvas + { Context = _context }; } @@ -27,32 +30,41 @@ public GtkBitmapExportContext(int width, int height, float dpi) : base(width, he /// writes a pixbuf to stream /// /// - public override void WriteToStream(Stream stream) { - if (_pixbuf != null) { + public override void WriteToStream(Stream stream) + { + if (_pixbuf != null) + { _pixbuf.SaveToStream(stream, Format); - } else { + } + else + { _pixbuf = _surface.SaveToStream(stream, Format); } } private GtkImage? _image; - public override IImage? Image { - get { + public override IImage? Image + { + get + { _pixbuf ??= _surface.CreatePixbuf(); - if (_pixbuf != null) return _image ??= new GtkImage(_pixbuf); + if (_pixbuf != null) + return _image ??= new GtkImage(_pixbuf); return _image; } } - public override void Dispose() { + public override void Dispose() + { _canvas?.Dispose(); _context?.Dispose(); _surface?.Dispose(); - if (_pixbuf != null) { + if (_pixbuf != null) + { var previousValue = Interlocked.Exchange(ref _pixbuf, null); previousValue?.Dispose(); } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/GtkImageLoadingService.cs b/src/Graphics/src/Graphics.Gtk/Gtk/GtkImageLoadingService.cs index a681173d3aff..fae82bdfb8b4 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/GtkImageLoadingService.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/GtkImageLoadingService.cs @@ -2,11 +2,11 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; - public class GtkImageLoadingService : IImageLoadingService +public class GtkImageLoadingService : IImageLoadingService +{ + public IImage FromStream(Stream stream, ImageFormat formatHint = ImageFormat.Png) { - public IImage FromStream(Stream stream, ImageFormat formatHint = ImageFormat.Png) - { - return GtkImage.FromStream(stream, formatHint); - } + return GtkImage.FromStream(stream, formatHint); } +} diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/HardwareInformations.cs b/src/Graphics/src/Graphics.Gtk/Gtk/HardwareInformations.cs index c7da717f44b6..3aa96ee0646c 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/HardwareInformations.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/HardwareInformations.cs @@ -3,7 +3,8 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class HardwareInformations { +public static class HardwareInformations +{ public static Gdk.Screen DefaultScreen => Gdk.Screen.Default; @@ -42,9 +43,11 @@ public static class HardwareInformations { public static int CurrentScaleFaktor = CurrentMonitor.ScaleFactor; - public static IEnumerable GetMonitors() { + public static IEnumerable GetMonitors() + { - for (var i = 0; i < DefaultDisplay.NMonitors; i++) { + for (var i = 0; i < DefaultDisplay.NMonitors; i++) + { yield return DefaultDisplay.GetMonitor(i); } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/ImageExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/ImageExtensions.cs index 16248aab1aaf..aebcfe0a68a2 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/ImageExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/ImageExtensions.cs @@ -3,10 +3,12 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class ImageExtensions { +public static class ImageExtensions +{ public static string ToImageExtension(this ImageFormat imageFormat) => - imageFormat switch { + imageFormat switch + { ImageFormat.Bmp => "bmp", ImageFormat.Png => "png", ImageFormat.Jpeg => "jpeg", @@ -15,30 +17,38 @@ public static string ToImageExtension(this ImageFormat imageFormat) => _ => throw new ArgumentOutOfRangeException(nameof(imageFormat), imageFormat, null) }; - public static Gdk.Pixbuf? SaveToStream(this Cairo.ImageSurface? surface, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + public static Gdk.Pixbuf? SaveToStream(this Cairo.ImageSurface? surface, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) + { if (surface == null) return null; - try { + try + { var px = surface.CreatePixbuf(); SaveToStream(px, stream, format, quality); return px; - } catch (Exception ex) { + } + catch (Exception ex) + { System.Diagnostics.Debug.WriteLine(ex); return default; } } - public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) + { if (pixbuf == null) return false; - try { + try + { var puf = pixbuf.SaveToBuffer(format.ToImageExtension()); stream.Write(puf, 0, puf.Length); puf = null; - } catch (Exception ex) { + } + catch (Exception ex) + { System.Diagnostics.Debug.WriteLine(ex); return false; @@ -47,7 +57,8 @@ public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFor return true; } - public static Gdk.Pixbuf? CreatePixbuf(this Cairo.ImageSurface? surface) { + public static Gdk.Pixbuf? CreatePixbuf(this Cairo.ImageSurface? surface) + { if (surface == null) return null; @@ -60,18 +71,21 @@ public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFor var stride = surface.Stride; var ncols = surface.Width; - if (BitConverter.IsLittleEndian) { + if (BitConverter.IsLittleEndian) + { var row = surface.Height; - while (row-- > 0) { + while (row-- > 0) + { var prevPos = n; var col = ncols; - while (col-- > 0) { + while (col-- > 0) + { var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1; - pixData[i] = (byte) (surfaceData[n + 2] * alphaFactor + 0.5); - pixData[i + 1] = (byte) (surfaceData[n + 1] * alphaFactor + 0.5); - pixData[i + 2] = (byte) (surfaceData[n + 0] * alphaFactor + 0.5); + pixData[i] = (byte)(surfaceData[n + 2] * alphaFactor + 0.5); + pixData[i + 1] = (byte)(surfaceData[n + 1] * alphaFactor + 0.5); + pixData[i + 2] = (byte)(surfaceData[n + 0] * alphaFactor + 0.5); if (nbytes == 4) pixData[i + 3] = surfaceData[n + 3]; @@ -82,18 +96,22 @@ public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFor n = prevPos + stride; } - } else { + } + else + { var row = surface.Height; - while (row-- > 0) { + while (row-- > 0) + { var prevPos = n; var col = ncols; - while (col-- > 0) { + while (col-- > 0) + { var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1; - pixData[i] = (byte) (surfaceData[n + 1] * alphaFactor + 0.5); - pixData[i + 1] = (byte) (surfaceData[n + 2] * alphaFactor + 0.5); - pixData[i + 2] = (byte) (surfaceData[n + 3] * alphaFactor + 0.5); + pixData[i] = (byte)(surfaceData[n + 1] * alphaFactor + 0.5); + pixData[i + 1] = (byte)(surfaceData[n + 2] * alphaFactor + 0.5); + pixData[i + 2] = (byte)(surfaceData[n + 3] * alphaFactor + 0.5); if (nbytes == 4) pixData[i + 3] = surfaceData[n + 0]; @@ -109,13 +127,14 @@ public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFor return new Gdk.Pixbuf(pixData, Gdk.Colorspace.Rgb, nbytes == 4, 8, surface.Width, surface.Height, surface.Width * nbytes, null); } - public static Cairo.Pattern? CreatePattern(this Gdk.Pixbuf? pixbuf, double scaleFactor) { + public static Cairo.Pattern? CreatePattern(this Gdk.Pixbuf? pixbuf, double scaleFactor) + { if (pixbuf == null) return null; - using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int) (pixbuf.Width * scaleFactor), (int) (pixbuf.Height * scaleFactor)); + using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int)(pixbuf.Width * scaleFactor), (int)(pixbuf.Height * scaleFactor)); using var context = new Cairo.Context(surface); - context.Scale(surface.Width / (double) pixbuf.Width, surface.Height / (double) pixbuf.Height); + context.Scale(surface.Width / (double)pixbuf.Width, surface.Height / (double)pixbuf.Height); Gdk.CairoHelper.SetSourcePixbuf(context, pixbuf, 0, 0); context.Paint(); surface.Flush(); diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/LineBreakMode.cs b/src/Graphics/src/Graphics.Gtk/Gtk/LineBreakMode.cs index 4b6e4995ccc9..6d6a93315ea8 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/LineBreakMode.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/LineBreakMode.cs @@ -6,7 +6,8 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; [Flags] -public enum LineBreakMode { +public enum LineBreakMode +{ None = 0, diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PaintExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PaintExtensions.cs index a85568da88b4..a41627e7e00b 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PaintExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PaintExtensions.cs @@ -2,16 +2,19 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public static class PaintExtensions { +public static class PaintExtensions +{ - public static Cairo.Context? PaintToSurface(this PatternPaint? it, Cairo.Surface? surface, float scale) { + public static Cairo.Context? PaintToSurface(this PatternPaint? it, Cairo.Surface? surface, float scale) + { if (surface == null || it == null) return null; var context = new Cairo.Context(surface); context.Scale(scale, scale); - using var canv = new PlatformCanvas { + using var canv = new PlatformCanvas + { Context = context, }; @@ -35,11 +38,12 @@ REPEAT the pattern is tiled by repeating public static void SetCairoExtend(Cairo.Extend it) { } - public static Gdk.Pixbuf? GetPatternBitmap(this PatternPaint? it, float scale) { + public static Gdk.Pixbuf? GetPatternBitmap(this PatternPaint? it, float scale) + { if (it == null) return null; - using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int) it.Pattern.Width, (int) it.Pattern.Height); + using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int)it.Pattern.Width, (int)it.Pattern.Height); using var context = it.PaintToSurface(surface, scale); surface.Flush(); @@ -51,7 +55,8 @@ public static void SetCairoExtend(Cairo.Extend it) { } /// does not work, pattern isn't shown /// [GtkMissingImplementation] - public static Cairo.Pattern? GetCairoPattern(this PatternPaint? it, Cairo.Surface? surface, float scale) { + public static Cairo.Pattern? GetCairoPattern(this PatternPaint? it, Cairo.Surface? surface, float scale) + { if (surface == null || it == null) return null; @@ -63,7 +68,8 @@ public static void SetCairoExtend(Cairo.Extend it) { } return pattern; } - public static Cairo.Pattern? GetCairoPattern(this LinearGradientPaint? it, RectF rectangle, float scaleFactor) { + public static Cairo.Pattern? GetCairoPattern(this LinearGradientPaint? it, RectF rectangle, float scaleFactor) + { if (it == null) return null; @@ -76,14 +82,16 @@ public static void SetCairoExtend(Cairo.Extend it) { } // https://developer.gnome.org/cairo/stable/cairo-cairo-pattern-t.html#cairo-pattern-create-linear var pattern = new Cairo.LinearGradient(x1, y1, x2, y2); - foreach (var s in it.GetSortedStops()) { + foreach (var s in it.GetSortedStops()) + { pattern.AddColorStop(s.Offset, s.Color.ToCairoColor()); } return pattern; } - public static Cairo.Pattern? GetCairoPattern(this RadialGradientPaint? it, RectF rectangle, float scaleFactor) { + public static Cairo.Pattern? GetCairoPattern(this RadialGradientPaint? it, RectF rectangle, float scaleFactor) + { if (it == null) return null; @@ -102,7 +110,8 @@ public static void SetCairoExtend(Cairo.Extend it) { } // https://developer.gnome.org/cairo/stable/cairo-cairo-pattern-t.html#cairo-pattern-create-radial var pattern = new Cairo.RadialGradient(x1, y1, radius1, x2, y2, radius2); - foreach (var s in it.GetSortedStops()) { + foreach (var s in it.GetSortedStops()) + { pattern.AddColorStop(s.Offset, s.Color.ToCairoColor()); } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Context.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Context.cs index 22fa5fd932a2..e3cab1de3412 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Context.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Context.cs @@ -2,9 +2,11 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public partial class PlatformCanvas { +public partial class PlatformCanvas +{ - private Cairo.Surface CreateSurface(Cairo.Context context, bool imageSurface = false) { + private Cairo.Surface CreateSurface(Cairo.Context context, bool imageSurface = false) + { var surface = context.GetTarget(); var extents = context.PathExtents(); @@ -13,19 +15,21 @@ private Cairo.Surface CreateSurface(Cairo.Context context, bool imageSurface = f var s = surface.GetSize(); var shadowSurface = s.HasValue && !imageSurface ? - surface.CreateSimilar(surface.Content, (int) pathSize.Width, (int) pathSize.Height) : - new Cairo.ImageSurface(Cairo.Format.ARGB32, (int) pathSize.Width, (int) pathSize.Height); + surface.CreateSimilar(surface.Content, (int)pathSize.Width, (int)pathSize.Height) : + new Cairo.ImageSurface(Cairo.Format.ARGB32, (int)pathSize.Width, (int)pathSize.Height); return shadowSurface; } - private void AddLine(Cairo.Context context, float x1, float y1, float x2, float y2) { + private void AddLine(Cairo.Context context, float x1, float y1, float x2, float y2) + { context.MoveTo(x1, y1); context.LineTo(x2, y2); } - private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise, bool closed) { + private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise, bool closed) + { AddArc(context, x, y, width, height, startAngle, endAngle, clockwise); @@ -33,7 +37,8 @@ private void AddArc(Cairo.Context context, float x, float y, float width, float context.ClosePath(); } - private void AddRectangle(Cairo.Context context, float x, float y, float width, float height) { + private void AddRectangle(Cairo.Context context, float x, float y, float width, float height) + { context.Rectangle(x, y, width, height); } @@ -42,7 +47,8 @@ private void AddRectangle(Cairo.Context context, float x, float y, float width, /// private const double mRadians = System.Math.PI / 180d; - private void AddRoundedRectangle(Cairo.Context context, float left, float top, float width, float height, float radius) { + private void AddRoundedRectangle(Cairo.Context context, float left, float top, float width, float height, float radius) + { context.NewPath(); // top left @@ -56,7 +62,8 @@ private void AddRoundedRectangle(Cairo.Context context, float left, float top, f context.ClosePath(); } - public void AddEllipse(Cairo.Context context, float x, float y, float width, float height) { + public void AddEllipse(Cairo.Context context, float x, float y, float width, float height) + { context.Save(); context.NewPath(); @@ -66,7 +73,8 @@ public void AddEllipse(Cairo.Context context, float x, float y, float width, flo context.Restore(); } - private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise) { + private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise) + { // https://developer.gnome.org/cairo/stable/cairo-Paths.html#cairo-arc // Angles are measured in radians @@ -86,7 +94,8 @@ private void AddArc(Cairo.Context context, float x, float y, float width, float if (clockwise) context.Arc(0, 0, r, startAngleInRadians, endAngleInRadians); - else { + else + { context.ArcNegative(0, 0, r, startAngleInRadians, endAngleInRadians); } @@ -94,20 +103,27 @@ private void AddArc(Cairo.Context context, float x, float y, float width, float } - private void AddPath(Cairo.Context context, PathF target) { + private void AddPath(Cairo.Context context, PathF target) + { var pointIndex = 0; var arcAngleIndex = 0; var arcClockwiseIndex = 0; - foreach (var type in target.SegmentTypes) { - if (type == PathOperation.Move) { + foreach (var type in target.SegmentTypes) + { + if (type == PathOperation.Move) + { var point = target[pointIndex++]; context.MoveTo(point.X, point.Y); - } else if (type == PathOperation.Line) { + } + else if (type == PathOperation.Line) + { var endPoint = target[pointIndex++]; context.LineTo(endPoint.X, endPoint.Y); - } else if (type == PathOperation.Quad) { + } + else if (type == PathOperation.Quad) + { var p1 = pointIndex > 0 ? target[pointIndex - 1] : context.CurrentPoint.ToPointF(); var c = target[pointIndex++]; var p2 = target[pointIndex++]; @@ -125,7 +141,9 @@ private void AddPath(Cairo.Context context, PathF target) { c2.X, c2.Y, p2.X, p2.Y); - } else if (type == PathOperation.Cubic) { + } + else if (type == PathOperation.Cubic) + { var controlPoint1 = target[pointIndex++]; var controlPoint2 = target[pointIndex++]; var endPoint = target[pointIndex++]; @@ -139,7 +157,9 @@ private void AddPath(Cairo.Context context, PathF target) { controlPoint2.X, controlPoint2.Y, endPoint.X, endPoint.Y); - } else if (type == PathOperation.Arc) { + } + else if (type == PathOperation.Arc) + { var topLeft = target[pointIndex++]; var bottomRight = target[pointIndex++]; var startAngle = target.GetArcAngle(arcAngleIndex++); @@ -148,25 +168,32 @@ private void AddPath(Cairo.Context context, PathF target) { AddArc(context, topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y, startAngle, endAngle, clockwise); - } else if (type == PathOperation.Close) { + } + else if (type == PathOperation.Close) + { context.ClosePath(); } } } - public void DrawPixbuf(Cairo.Context context, Gdk.Pixbuf pixbuf, double x, double y, double width, double height) { + public void DrawPixbuf(Cairo.Context context, Gdk.Pixbuf pixbuf, double x, double y, double width, double height) + { context.Save(); context.Translate(x, y); context.Scale(width / pixbuf.Width, height / pixbuf.Height); Gdk.CairoHelper.SetSourcePixbuf(context, pixbuf, 0, 0); - using (var p = context.GetSource()) { - if (p is Cairo.SurfacePattern pattern) { - if (width > pixbuf.Width || height > pixbuf.Height) { + using (var p = context.GetSource()) + { + if (p is Cairo.SurfacePattern pattern) + { + if (width > pixbuf.Width || height > pixbuf.Height) + { // Fixes blur issue when rendering on an image surface pattern.Filter = Cairo.Filter.Fast; - } else + } + else pattern.Filter = Cairo.Filter.Good; } } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Patterns.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Patterns.cs index d023eb1cd3f2..77a0a10c9db8 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Patterns.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Patterns.cs @@ -2,55 +2,75 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public partial class PlatformCanvas { +public partial class PlatformCanvas +{ - public void DrawFillPaint(Cairo.Context? context, Paint? paint, RectF rectangle) { + public void DrawFillPaint(Cairo.Context? context, Paint? paint, RectF rectangle) + { if (paint == null || context == null) return; - switch (paint) { + switch (paint) + { - case SolidPaint solidPaint: { - FillColor = solidPaint.Color; + case SolidPaint solidPaint: + { + FillColor = solidPaint.Color; + context.SetSourceColor(solidPaint.Color.ToCairoColor()); + break; + } - break; - } - - case LinearGradientPaint linearGradientPaint: { - try { - if (linearGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) { - context.SetSource(pattern); - pattern.Dispose(); - } else { - FillColor = paint.BackgroundColor; + case LinearGradientPaint linearGradientPaint: + { + try + { + if (linearGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) + { + context.SetSource(pattern); + pattern.Dispose(); + } + else + { + FillColor = paint.BackgroundColor; + } + } + catch (Exception exc) + { + System.Diagnostics.Debug.WriteLine(exc); + FillColor = linearGradientPaint.BlendStartAndEndColors(); } - } catch (Exception exc) { - System.Diagnostics.Debug.WriteLine(exc); - FillColor = linearGradientPaint.BlendStartAndEndColors(); - } - break; - } + break; + } - case RadialGradientPaint radialGradientPaint: { + case RadialGradientPaint radialGradientPaint: + { - try { - if (radialGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) { - context.SetSource(pattern); - pattern.Dispose(); - } else { - FillColor = paint.BackgroundColor; + try + { + if (radialGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) + { + context.SetSource(pattern); + pattern.Dispose(); + } + else + { + FillColor = paint.BackgroundColor; + } + } + catch (Exception exc) + { + System.Diagnostics.Debug.WriteLine(exc); + FillColor = radialGradientPaint.BlendStartAndEndColors(); } - } catch (Exception exc) { - System.Diagnostics.Debug.WriteLine(exc); - FillColor = radialGradientPaint.BlendStartAndEndColors(); - } - break; - } + break; + } - case PatternPaint patternPaint: { - try { + case PatternPaint patternPaint: + { + try + { #if UseSurfacePattern // would be nice to have: draw pattern without creating a pixpuf: @@ -67,43 +87,53 @@ public void DrawFillPaint(Cairo.Context? context, Paint? paint, RectF rectangle) FillColor = paint.BackgroundColor; } #else - using var pixbuf = patternPaint.GetPatternBitmap(DisplayScale); + using var pixbuf = patternPaint.GetPatternBitmap(DisplayScale); - if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) { - pattern.Extend = Cairo.Extend.Repeat; - context.SetSource(pattern); - pattern.Dispose(); - } + if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) + { + pattern.Extend = Cairo.Extend.Repeat; + context.SetSource(pattern); + pattern.Dispose(); + } #endif - } catch (Exception exc) { - System.Diagnostics.Debug.WriteLine(exc); - FillColor = paint.BackgroundColor; - } + } + catch (Exception exc) + { + System.Diagnostics.Debug.WriteLine(exc); + FillColor = paint.BackgroundColor; + } - break; - } + break; + } - case ImagePaint {Image: GtkImage image} imagePaint: { - var pixbuf = image.NativeImage; + case ImagePaint { Image: GtkImage image } imagePaint: + { + var pixbuf = image.NativeImage; - if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) { - try { + if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) + { + try + { - context.SetSource(pattern); - pattern.Dispose(); + context.SetSource(pattern); + pattern.Dispose(); - } catch (Exception exc) { - System.Diagnostics.Debug.WriteLine(exc); - FillColor = paint.BackgroundColor; + } + catch (Exception exc) + { + System.Diagnostics.Debug.WriteLine(exc); + FillColor = paint.BackgroundColor; + } + } + else + { + FillColor = paint.BackgroundColor ?? Colors.White; } - } else { - FillColor = paint.BackgroundColor ?? Colors.White; - } - break; - } + break; + } case ImagePaint imagePaint: FillColor = paint.BackgroundColor ?? Colors.White; diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Shadow.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Shadow.cs index d460f44f7c91..09e16ae935d3 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Shadow.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.Shadow.cs @@ -1,10 +1,13 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public partial class PlatformCanvas { +public partial class PlatformCanvas +{ - public void DrawShadow(bool fill) { + public void DrawShadow(bool fill) + { - if (CurrentState.Shadow != default) { + if (CurrentState.Shadow != default) + { using var path = Context.CopyPath(); @@ -27,7 +30,8 @@ public void DrawShadow(bool fill) { if (true) shadowCtx.PaintWithAlpha(0.3); - else { + else + { shadowCtx.LineWidth = 10; shadowCtx.Stroke(); } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.cs index 3927557ca7f6..e68167cd3e8f 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvas.cs @@ -5,7 +5,7 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; public partial class PlatformCanvas : AbstractCanvas { public PlatformCanvas() - : base(new PlatformCanvasStateService(), new PlatformStringSizeService ()) + : base(new PlatformCanvasStateService(), new PlatformStringSizeService()) { } diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasState.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasState.cs index ed66f64724b7..586027a3b23f 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasState.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasState.cs @@ -2,9 +2,11 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public class PlatformCanvasState : CanvasState { +public class PlatformCanvasState : CanvasState +{ - public PlatformCanvasState() { + public PlatformCanvasState() + { Alpha = 1; StrokeColor = Colors.Black.ToCairoColor(); @@ -62,13 +64,14 @@ public PlatformCanvasState(PlatformCanvasState prototype) private readonly double[] zerodash = new double[0]; - public double[] NativeDash => StrokeDashPattern != null ? Array.ConvertAll(StrokeDashPattern, f => (double) f) : zerodash; + public double[] NativeDash => StrokeDashPattern != null ? Array.ConvertAll(StrokeDashPattern, f => (double)f) : zerodash; public (SizeF offset, float blur, Color color) Shadow { get; set; } public (Paint paint, RectF rectangle) FillPaint { get; set; } - public override void Dispose() { + public override void Dispose() + { FillPaint = default; Shadow = default; diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasStateService.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasStateService.cs index 4af0716a209b..66150e741ae1 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasStateService.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformCanvasStateService.cs @@ -2,8 +2,9 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public class PlatformCanvasStateService : ICanvasStateService { - public PlatformCanvasState CreateNew (object context) => new() { }; +public class PlatformCanvasStateService : ICanvasStateService +{ + public PlatformCanvasState CreateNew(object context) => new() { }; - public PlatformCanvasState CreateCopy (PlatformCanvasState prototype) => new(prototype); + public PlatformCanvasState CreateCopy(PlatformCanvasState prototype) => new(prototype); } \ No newline at end of file diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformStringSizeService.cs b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformStringSizeService.cs index 085aacd07900..f750db08349d 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/PlatformStringSizeService.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/PlatformStringSizeService.cs @@ -1,13 +1,17 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -public class PlatformStringSizeService : IStringSizeService { +public class PlatformStringSizeService : IStringSizeService +{ Cairo.Context? _sharedContext; - public Cairo.Context SharedContext { - get { - if (_sharedContext == null) { - using var sf = new Cairo.ImageSurface (Cairo.Format.ARGB32, 1, 1); - _sharedContext = new Cairo.Context (sf); + public Cairo.Context SharedContext + { + get + { + if (_sharedContext == null) + { + using var sf = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + _sharedContext = new Cairo.Context(sf); } return _sharedContext; @@ -16,32 +20,37 @@ public Cairo.Context SharedContext { private static TextLayout? _textLayout; - private TextLayout SharedTextLayout => _textLayout ??= new TextLayout (SharedContext) { + private TextLayout SharedTextLayout => _textLayout ??= new TextLayout(SharedContext) + { HeightForWidth = true }; - public SizeF GetStringSize (string value, IFont font, float fontSize) { - if (string.IsNullOrEmpty (value)) - return new SizeF (); + public SizeF GetStringSize(string value, IFont font, float fontSize) + { + if (string.IsNullOrEmpty(value)) + return new SizeF(); - lock (SharedTextLayout) { - SharedTextLayout.SetFontStyle (font, fontSize); - var size = SharedTextLayout.GetPixelSize (value); - return new SizeF (size.width, size.height); + lock (SharedTextLayout) + { + SharedTextLayout.SetFontStyle(font, fontSize); + var size = SharedTextLayout.GetPixelSize(value); + return new SizeF(size.width, size.height); } } - public SizeF GetStringSize (string value, IFont font, float aFontSize, HorizontalAlignment aHorizontalAlignment, VerticalAlignment aVerticalAlignment) { - if (string.IsNullOrEmpty (value)) - return new SizeF (); + public SizeF GetStringSize(string value, IFont font, float aFontSize, HorizontalAlignment aHorizontalAlignment, VerticalAlignment aVerticalAlignment) + { + if (string.IsNullOrEmpty(value)) + return new SizeF(); - lock (SharedTextLayout) { - SharedTextLayout.SetFontStyle (font, aFontSize); + lock (SharedTextLayout) + { + SharedTextLayout.SetFontStyle(font, aFontSize); SharedTextLayout.HorizontalAlignment = aHorizontalAlignment; SharedTextLayout.VerticalAlignment = aVerticalAlignment; - var size = SharedTextLayout.GetPixelSize (value); - return new SizeF (size.width, size.height); + var size = SharedTextLayout.GetPixelSize(value); + return new SizeF(size.width, size.height); } } } \ No newline at end of file diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/TextLayout.cs b/src/Graphics/src/Graphics.Gtk/Gtk/TextLayout.cs index 76352eed623b..24dfa81d19c0 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/TextLayout.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/TextLayout.cs @@ -10,69 +10,83 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; /// https://developer.gnome.org/pango/1.46/pango-Layout-Objects.html /// https://developer.gnome.org/gdk3/stable/gdk3-Pango-Interaction.html /// -public class TextLayout : IDisposable { +public class TextLayout : IDisposable +{ private Context _context; - public TextLayout(Context context) { + public TextLayout(Context context) + { _context = context; } public Context Context => _context; - public string FontFamily { - get { - if (string.IsNullOrEmpty (_fontFamily)) { - _fontFamily = Gdk.PangoHelper.ContextGet ().FontDescription.Family; + public string FontFamily + { + get + { + if (string.IsNullOrEmpty(_fontFamily)) + { + _fontFamily = Gdk.PangoHelper.ContextGet().FontDescription.Family; } return _fontFamily; } - set { - if (string.Equals (_fontFamily, value)) + set + { + if (string.Equals(_fontFamily, value)) return; _fontFamily = value; - FontDescriptionChanged (); + FontDescriptionChanged(); } } - public Pango.Weight Weight { + public Pango.Weight Weight + { get => _weight; - set { + set + { if (_weight == value) return; _weight = value; - FontDescriptionChanged (); + FontDescriptionChanged(); } } - public Pango.Style Style { + public Pango.Style Style + { get => _style; - set { + set + { if (_style == value) return; _style = value; - FontDescriptionChanged (); + FontDescriptionChanged(); } } - public int PangoFontSize { - get { - if (_pangoFontSize == -1) { - _pangoFontSize = Gdk.PangoHelper.ContextGet ().FontDescription.Size; + public int PangoFontSize + { + get + { + if (_pangoFontSize == -1) + { + _pangoFontSize = Gdk.PangoHelper.ContextGet().FontDescription.Size; } return _pangoFontSize; } - set { + set + { if (_pangoFontSize == value) return; _pangoFontSize = value; - FontDescriptionChanged (); + FontDescriptionChanged(); } } @@ -97,15 +111,18 @@ public int PangoFontSize { public Action AfterDrawn { get; set; } - public void SetLayout(Pango.Layout value) { + public void SetLayout(Pango.Layout value) + { _layout = value; _layoutOwned = false; } private Pango.FontDescription? _fontDescription; - void FontDescriptionChanged () { - if (_fontDescriptionOwned && _fontDescription is {}) { + void FontDescriptionChanged() + { + if (_fontDescriptionOwned && _fontDescription is { }) + { _fontDescription.Family = FontFamily; _fontDescription.Weight = Weight; _fontDescription.Style = Style; @@ -119,10 +136,14 @@ void FontDescriptionChanged () { private Style _style = Pango.Style.Normal; private int _pangoFontSize = -1; - public Pango.FontDescription FontDescription { - get { - if (_fontDescription == null) { - _fontDescription = new Pango.FontDescription { + public Pango.FontDescription FontDescription + { + get + { + if (_fontDescription == null) + { + _fontDescription = new Pango.FontDescription + { Family = FontFamily, Weight = Weight, Style = Style, @@ -134,21 +155,25 @@ public Pango.FontDescription FontDescription { return _fontDescription; } - set { - if (Equals (_fontDescription, value)) + set + { + if (Equals(_fontDescription, value)) return; _fontDescription = value; _fontDescriptionOwned = false; } } - public Pango.Layout GetLayout() { - if (_layout == null) { + public Pango.Layout GetLayout() + { + if (_layout == null) + { _layout = Pango.CairoHelper.CreateLayout(Context); _layoutOwned = true; } - if (_layout.FontDescription != FontDescription) { + if (_layout.FontDescription != FontDescription) + { _layout.FontDescription = FontDescription; } @@ -162,24 +187,32 @@ public Pango.Layout GetLayout() { return _layout; } - public void Dispose() { - if (_fontDescriptionOwned) { + public void Dispose() + { + if (_fontDescriptionOwned) + { _fontDescription?.Dispose(); } - if (_layoutOwned) { + if (_layoutOwned) + { _layout?.Dispose(); } } - public (int width, int height) GetPixelSize(string text, double desiredSize = -1d) { + public (int width, int height) GetPixelSize(string text, double desiredSize = -1d) + { var layout = GetLayout(); - if (desiredSize > 0) { - if (HeightForWidth) { + if (desiredSize > 0) + { + if (HeightForWidth) + { layout.Width = desiredSize.ScaledToPango(); - } else { + } + else + { layout.Height = desiredSize.ScaledToPango(); } } @@ -190,7 +223,8 @@ public void Dispose() { return (textWidth, textHeight); } - private void Draw() { + private void Draw() + { if (_layout == null) return; @@ -204,35 +238,40 @@ private void Draw() { Pango.CairoHelper.ShowLayout(Context, _layout); } - private float GetX(float x, int width) => HorizontalAlignment switch { + private float GetX(float x, int width) => HorizontalAlignment switch + { HorizontalAlignment.Left => x, HorizontalAlignment.Right => x - width, HorizontalAlignment.Center => x - width / 2f, _ => x }; - private float GetY(float y, int height) => VerticalAlignment switch { + private float GetY(float y, int height) => VerticalAlignment switch + { VerticalAlignment.Top => y, VerticalAlignment.Center => y - height, VerticalAlignment.Bottom => y - height / 2f, _ => y }; - private float GetDx(int width) => HorizontalAlignment switch { + private float GetDx(int width) => HorizontalAlignment switch + { HorizontalAlignment.Left => 0, HorizontalAlignment.Center => width / 2f, HorizontalAlignment.Right => width, _ => 0 }; - private float GetDy(int height) => VerticalAlignment switch { + private float GetDy(int height) => VerticalAlignment switch + { VerticalAlignment.Top => 0, VerticalAlignment.Center => height / 2f, VerticalAlignment.Bottom => height, _ => 0 }; - public void DrawString(string value, float x, float y) { + public void DrawString(string value, float x, float y) + { Context.Save(); @@ -240,7 +279,8 @@ public void DrawString(string value, float x, float y) { layout.SetText(value); layout.GetPixelSize(out var textWidth, out var textHeight); - if (layout.IsWrapped || layout.IsEllipsized) { + if (layout.IsWrapped || layout.IsEllipsized) + { if (HeightForWidth) layout.Width = textWidth.ScaledToPango(); else @@ -255,7 +295,8 @@ public void DrawString(string value, float x, float y) { } - public void DrawString(string value, float x, float y, float width, float height) { + public void DrawString(string value, float x, float y, float width, float height) + { Context.Save(); Context.Translate(x, y); @@ -263,25 +304,32 @@ public void DrawString(string value, float x, float y, float width, float height var layout = GetLayout(); layout.SetText(value); - if (HeightForWidth) { + if (HeightForWidth) + { layout.Width = width.ScaledToPango(); - if (TextFlow == TextFlow.ClipBounds) { + if (TextFlow == TextFlow.ClipBounds) + { layout.Height = height.ScaledToPango(); } - } else { + } + else + { layout.Height = height.ScaledToPango(); - if (TextFlow == TextFlow.ClipBounds) { + if (TextFlow == TextFlow.ClipBounds) + { layout.Width = width.ScaledToPango(); } } - if (TextFlow == TextFlow.ClipBounds && !layout.IsEllipsized) { + if (TextFlow == TextFlow.ClipBounds && !layout.IsEllipsized) + { layout.Ellipsize = Pango.EllipsizeMode.End; } - if (!layout.IsWrapped || !layout.IsEllipsized) { + if (!layout.IsWrapped || !layout.IsEllipsized) + { layout.Wrap = Pango.WrapMode.Char; } @@ -290,16 +338,17 @@ public void DrawString(string value, float x, float y, float width, float height var mX = HeightForWidth ? 0 : TextFlow == TextFlow.ClipBounds ? - Math.Max(0, GetDx((int) width - logicalRect.Width - logicalRect.X)) : - GetDx((int) width - logicalRect.Width - inkRect.X); + Math.Max(0, GetDx((int)width - logicalRect.Width - logicalRect.X)) : + GetDx((int)width - logicalRect.Width - inkRect.X); var mY = !HeightForWidth ? 0 : TextFlow == TextFlow.ClipBounds ? - Math.Max(0, GetDy((int) height - inkRect.Height - inkRect.Y)) : - GetDy((int) height - inkRect.Height - inkRect.Y); + Math.Max(0, GetDy((int)height - inkRect.Height - inkRect.Y)) : + GetDy((int)height - inkRect.Height - inkRect.Y); - if (mY + inkRect.Height > height && TextFlow == TextFlow.ClipBounds && !HeightForWidth) { + if (mY + inkRect.Height > height && TextFlow == TextFlow.ClipBounds && !HeightForWidth) + { mY = 0; } @@ -316,7 +365,8 @@ public void DrawAttributedText(IAttributedText value, float f, float f1, float w /// /// future use for /// - private void ClampToContext() { + private void ClampToContext() + { if (_layout == null || _context == null) return; @@ -326,8 +376,10 @@ private void ClampToContext() { var maxW = ctxSize.Width.ScaledToPango(); var maxH = ctxSize.Height.ScaledToPango(); - while (logicalRect.Width > maxW) { - if (!_layout.IsWrapped) { + while (logicalRect.Width > maxW) + { + if (!_layout.IsWrapped) + { _layout.Wrap = Pango.WrapMode.Char; } @@ -336,8 +388,10 @@ private void ClampToContext() { maxW -= 1.ScaledToPango(); } - while (logicalRect.Height > maxH) { - if (!_layout.IsWrapped) { + while (logicalRect.Height > maxH) + { + if (!_layout.IsWrapped) + { _layout.Wrap = Pango.WrapMode.Char; } @@ -353,7 +407,8 @@ private void ClampToContext() { /// /// Get the distance in pixels between the top of the layout bounds and the first line's baseline /// - public double GetBaseline() { + public double GetBaseline() + { // Just get the first line using var iter = GetLayout().Iter; @@ -363,7 +418,8 @@ public double GetBaseline() { /// /// Get the distance in pixels between the top of the layout bounds and the first line's meanline (usually equivalent to the baseline minus half of the x-height) /// - public double GetMeanline() { + public double GetMeanline() + { var baseline = 0; var layout = GetLayout(); diff --git a/src/Graphics/src/Graphics.Gtk/Gtk/TextLayoutExtensions.cs b/src/Graphics/src/Graphics.Gtk/Gtk/TextLayoutExtensions.cs index 134e3d28ad4f..46b9b11d109a 100644 --- a/src/Graphics/src/Graphics.Gtk/Gtk/TextLayoutExtensions.cs +++ b/src/Graphics/src/Graphics.Gtk/Gtk/TextLayoutExtensions.cs @@ -1,8 +1,10 @@ namespace Microsoft.Maui.Graphics.Platform.Gtk; -internal static class TextLayoutExtensions { +internal static class TextLayoutExtensions +{ - public static void SetFontStyle(this TextLayout it, IFont font, double? fontSize = null, int? weight = null, FontStyleType? fontStyleType = null) { + public static void SetFontStyle(this TextLayout it, IFont font, double? fontSize = null, int? weight = null, FontStyleType? fontStyleType = null) + { if (font is { }) it.FontFamily = font.Name; @@ -23,7 +25,8 @@ public static void SetFontStyle(this TextLayout it, IFont font, double? fontSize }; } - public static void SetCanvasState(this TextLayout it, PlatformCanvasState state) { + public static void SetCanvasState(this TextLayout it, PlatformCanvasState state) + { var font = (state?.Font ?? Font.Default).ToFontDescription(); @@ -34,19 +37,22 @@ public static void SetCanvasState(this TextLayout it, PlatformCanvasState state) it.TextColor = state?.FontColor ?? new Cairo.Color(); } - public static TextLayout WithCanvasState(this TextLayout it, PlatformCanvasState state) { + public static TextLayout WithCanvasState(this TextLayout it, PlatformCanvasState state) + { it.SetCanvasState(state); return it; } - public static Size GetSize(this TextLayout it, string text, float textHeigth) { - var (width, height) = it.GetPixelSize(text, (int) textHeigth); + public static Size GetSize(this TextLayout it, string text, float textHeigth) + { + var (width, height) = it.GetPixelSize(text, (int)textHeigth); return new Size(width, height); } - public static Pango.Alignment ToPango(this HorizontalAlignment it) => it switch { + public static Pango.Alignment ToPango(this HorizontalAlignment it) => it switch + { HorizontalAlignment.Center => Pango.Alignment.Center, HorizontalAlignment.Right => Pango.Alignment.Right, _ => Pango.Alignment.Left @@ -63,7 +69,8 @@ public static Pango.WrapMode ToPangoWrap(this LineBreakMode it) return Pango.WrapMode.Word; } - public static Pango.EllipsizeMode ToPangoEllipsize(this LineBreakMode it) { + public static Pango.EllipsizeMode ToPangoEllipsize(this LineBreakMode it) + { if (it.HasFlag(LineBreakMode.Ellipsis | LineBreakMode.Tail)) return Pango.EllipsizeMode.End;