From 71759eda7e910086f6ada025b4719307e43b495f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:38:03 +0000 Subject: [PATCH 01/13] Initial plan From 12952db3356114e76a611d5db09cff7d19c08486 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:50:50 +0000 Subject: [PATCH 02/13] Add Toolbar component with ToolbarButton items Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- CHANGELOG.md | 3 + .../Toolbar/ToolbarSamples.xaml | 82 ++++++++++++++++ .../Toolbar/ToolbarSamples.xaml.cs | 9 ++ .../Toolbar/ToolbarSamplesViewModel.cs | 24 +++++ .../Components/REGISTER_YOUR_SAMPLES_HERE.cs | 2 + src/library/DIPS.Mobile.UI/AssemblyInfo.cs | 1 + .../Components/Toolbar/Toolbar.Properties.cs | 23 +++++ .../Components/Toolbar/Toolbar.cs | 88 +++++++++++++++++ .../Toolbar/ToolbarButton.Properties.cs | 94 +++++++++++++++++++ .../Components/Toolbar/ToolbarButton.cs | 8 ++ 10 files changed, 334 insertions(+) create mode 100644 src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml create mode 100644 src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs create mode 100644 src/app/Components/ComponentsSamples/Toolbar/ToolbarSamplesViewModel.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.Properties.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 566fc8355..48fd75f4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [55.3.0] +- [Toolbar] Added `Toolbar` component with `ToolbarButton` items. The toolbar provides a cross-platform horizontal bar of icon buttons, aligned with Apple HIG toolbars and Material 3 toolbars. + ## [55.2.2] - [iOS26][Tip] Added more padding. diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml new file mode 100644 index 000000000..0b7550c48 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs new file mode 100644 index 000000000..b6cc63067 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs @@ -0,0 +1,9 @@ +namespace Components.ComponentsSamples.Toolbar; + +public partial class ToolbarSamples +{ + public ToolbarSamples() + { + InitializeComponent(); + } +} diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamplesViewModel.cs b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamplesViewModel.cs new file mode 100644 index 000000000..85ca80f36 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamplesViewModel.cs @@ -0,0 +1,24 @@ +using DIPS.Mobile.UI.MVVM; + +namespace Components.ComponentsSamples.Toolbar; + +internal class ToolbarSamplesViewModel : ViewModel +{ + private string m_lastAction = "None"; + + public Command EditCommand => new(OnEdit); + public Command SaveCommand => new(OnSave); + public Command FilterCommand => new(OnFilter); + public Command AddCommand => new(OnAdd); + + public string LastAction + { + get => m_lastAction; + set => RaiseWhenSet(ref m_lastAction, value); + } + + private void OnEdit() => LastAction = "Edit tapped"; + private void OnSave() => LastAction = "Save tapped"; + private void OnFilter() => LastAction = "Filter tapped"; + private void OnAdd() => LastAction = "Add tapped"; +} diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs index 0a0ec44ee..4f038643d 100644 --- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs +++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs @@ -21,6 +21,7 @@ using Components.ComponentsSamples.Sorting; using Components.ComponentsSamples.SyntaxHighlighting; using Components.ComponentsSamples.TabView; +using Components.ComponentsSamples.Toolbar; using Components.ComponentsSamples.Tags; using Components.ComponentsSamples.Text; using Components.ComponentsSamples.TextFields; @@ -73,6 +74,7 @@ public static List RegisterSamples() new(SampleType.Components, "Zoom Container", () => new PanZoomContainerSample()), new(SampleType.Components, "Gallery", () => new GallerySample()), new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()), + new(SampleType.Components, "Toolbar", () => new ToolbarSamples()), new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()), diff --git a/src/library/DIPS.Mobile.UI/AssemblyInfo.cs b/src/library/DIPS.Mobile.UI/AssemblyInfo.cs index 74687532f..b745213da 100644 --- a/src/library/DIPS.Mobile.UI/AssemblyInfo.cs +++ b/src/library/DIPS.Mobile.UI/AssemblyInfo.cs @@ -100,6 +100,7 @@ [assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.TiffViewer")] [assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.Gallery")] [assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Effects.Accessibility")] +[assembly: XmlnsDefinition("http://dips.com/mobile.ui","DIPS.Mobile.UI.Components.Toolbar")] diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs new file mode 100644 index 000000000..7456a7bb6 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs @@ -0,0 +1,23 @@ +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class Toolbar +{ + /// + /// + /// + public static readonly BindableProperty ButtonsProperty = BindableProperty.Create( + nameof(Buttons), + typeof(IList), + typeof(Toolbar), + defaultValueCreator: _ => new List(), + propertyChanged: (bindable, _, _) => ((Toolbar)bindable).OnButtonsChanged()); + + /// + /// The buttons to display in the toolbar. + /// + public IList Buttons + { + get => (IList)GetValue(ButtonsProperty); + set => SetValue(ButtonsProperty, value); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs new file mode 100644 index 000000000..08a9df65e --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs @@ -0,0 +1,88 @@ +using DIPS.Mobile.UI.Components.Buttons; +using DIPS.Mobile.UI.Components.Dividers; +using DIPS.Mobile.UI.Resources.Styles; +using DIPS.Mobile.UI.Resources.Styles.Button; + +namespace DIPS.Mobile.UI.Components.Toolbar; + +/// +/// A cross-platform toolbar component that displays a horizontal bar of icon buttons. +/// +/// +/// iOS: https://developer.apple.com/design/human-interface-guidelines/toolbars +/// Android: https://m3.material.io/components/toolbars/overview +/// +[ContentProperty(nameof(Buttons))] +public partial class Toolbar : ContentView +{ + private readonly Grid m_buttonsGrid = new(); + + public Toolbar() + { + this.SetAppThemeColor(BackgroundColorProperty, ColorName.color_surface_default); + + var topBorder = new Divider + { + HeightRequest = Sizes.GetSize(SizeName.stroke_small) + }; + + Content = new VerticalStackLayout + { + Spacing = 0, + Children = { topBorder, m_buttonsGrid } + }; + } + + private void OnButtonsChanged() + { + m_buttonsGrid.ColumnDefinitions.Clear(); + m_buttonsGrid.Children.Clear(); + + if (Buttons is null || Buttons.Count == 0) + return; + + for (var i = 0; i < Buttons.Count; i++) + { + m_buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); + var toolbarButton = Buttons[i]; + toolbarButton.BindingContext = BindingContext; + var buttonView = CreateButtonView(toolbarButton); + Grid.SetColumn(buttonView, i); + m_buttonsGrid.Add(buttonView); + } + } + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + if (Buttons is null) + return; + + foreach (var toolbarButton in Buttons) + { + toolbarButton.BindingContext = BindingContext; + } + } + + private static View CreateButtonView(ToolbarButton toolbarButton) + { + var button = new Button + { + Style = Styles.GetButtonStyle(ButtonStyle.GhostIconSmall), + HorizontalOptions = LayoutOptions.Center, + }; + + button.SetBinding(Button.ImageSourceProperty, new Binding(nameof(ToolbarButton.Icon), source: toolbarButton)); + button.SetBinding(IsEnabledProperty, new Binding(nameof(ToolbarButton.IsEnabled), source: toolbarButton)); + button.SetBinding(Button.CommandProperty, new Binding(nameof(ToolbarButton.Command), source: toolbarButton)); + button.SetBinding(Button.CommandParameterProperty, new Binding(nameof(ToolbarButton.CommandParameter), source: toolbarButton)); + + if (!string.IsNullOrEmpty(toolbarButton.Title)) + { + SemanticProperties.SetDescription(button, toolbarButton.Title); + } + + return button; + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.Properties.cs new file mode 100644 index 000000000..d203d1d57 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.Properties.cs @@ -0,0 +1,94 @@ +using System.ComponentModel; +using System.Windows.Input; + +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class ToolbarButton +{ + /// + /// + /// + public static readonly BindableProperty TitleProperty = BindableProperty.Create( + nameof(Title), + typeof(string), + typeof(ToolbarButton)); + + /// + /// + /// + public static readonly BindableProperty IconProperty = BindableProperty.Create( + nameof(Icon), + typeof(ImageSource), + typeof(ToolbarButton)); + + /// + /// + /// + public static readonly BindableProperty CommandProperty = BindableProperty.Create( + nameof(Command), + typeof(ICommand), + typeof(ToolbarButton)); + + /// + /// + /// + public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create( + nameof(CommandParameter), + typeof(object), + typeof(ToolbarButton)); + + /// + /// + /// + public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create( + nameof(IsEnabled), + typeof(bool), + typeof(ToolbarButton), + defaultValue: true); + + /// + /// The title of the toolbar button, used as the accessibility label. + /// + public string? Title + { + get => (string?)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + /// + /// The icon to display in the toolbar button. + /// + [TypeConverter(nameof(ImageSourceConverter))] + public ImageSource? Icon + { + get => (ImageSource?)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// The command to execute when the button is tapped. + /// + public ICommand? Command + { + get => (ICommand?)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// The parameter to pass to when the button is tapped. + /// + public object? CommandParameter + { + get => (object?)GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Determines whether the button is enabled. + /// + public bool IsEnabled + { + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.cs new file mode 100644 index 000000000..a7616251f --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarButton.cs @@ -0,0 +1,8 @@ +namespace DIPS.Mobile.UI.Components.Toolbar; + +/// +/// A button to display in a . +/// +public partial class ToolbarButton : Element +{ +} From 89d1d8964f938ce5d1a30b2a2b023c2277ce228a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:05:36 +0000 Subject: [PATCH 03/13] Rewrite Toolbar to use native platform components via handler pattern Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../API/Builder/AppHostBuilderExtensions.cs | 2 + .../Toolbar/Android/ToolbarHandler.cs | 120 ++++++++++++++++++ .../Components/Toolbar/Toolbar.cs | 67 +--------- .../Components/Toolbar/ToolbarHandler.cs | 16 +++ .../Toolbar/dotnet/ToolbarHandler.cs | 12 ++ .../Components/Toolbar/iOS/ToolbarHandler.cs | 68 ++++++++++ 6 files changed, 220 insertions(+), 65 deletions(-) create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs diff --git a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs index 3ba1f0a91..02c0cc710 100644 --- a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs +++ b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs @@ -9,6 +9,7 @@ using DIPS.Mobile.UI.Components.Navigation.FloatingNavigationButton; using DIPS.Mobile.UI.Components.PanZoomContainer; using DIPS.Mobile.UI.Components.Pickers.ScrollPicker; +using DIPS.Mobile.UI.Components.Toolbar; using DIPS.Mobile.UI.Effects.Animation.Effects; using DIPS.Mobile.UI.Effects.Touch; using DotNet.Meteor.HotReload.Plugin; @@ -73,6 +74,7 @@ public static MauiAppBuilder UseDIPSUI( handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); AddPlatformHandlers(handlers); }); diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs new file mode 100644 index 000000000..f8019228b --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs @@ -0,0 +1,120 @@ +using Android.Content.Res; +using Android.Views; +using Android.Widget; +using DIPS.Mobile.UI.API.Library; +using Google.Android.Material.Button; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; + +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class ToolbarHandler : ViewHandler +{ + private LinearLayout? m_buttonsLayout; + private readonly List<(MaterialButton Button, EventHandler Handler)> m_buttonHandlers = []; + + protected override LinearLayout CreatePlatformView() + { + // Outer vertical layout: [top border row] + [buttons row] + var outer = new LinearLayout(Context) + { + Orientation = Orientation.Vertical, + }; + outer.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform()); + + // Top border + var density = Context?.Resources?.DisplayMetrics?.Density ?? 1f; + var borderPx = (int)(Sizes.GetSize(SizeName.stroke_small) * density); + var border = new View(Context); + border.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_border_default).ToPlatform()); + outer.AddView(border, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, borderPx)); + + // Buttons row + m_buttonsLayout = new LinearLayout(Context) + { + Orientation = Orientation.Horizontal, + }; + outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent)); + + return outer; + } + + protected override void ConnectHandler(LinearLayout platformView) + { + base.ConnectHandler(platformView); + UpdateButtons(); + } + + protected override void DisconnectHandler(LinearLayout platformView) + { + UnsubscribeAllButtonHandlers(); + base.DisconnectHandler(platformView); + } + + private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) + { + handler.UpdateButtons(); + } + + private void UpdateButtons() + { + UnsubscribeAllButtonHandlers(); + m_buttonsLayout?.RemoveAllViews(); + + if (VirtualView.Buttons is null || m_buttonsLayout is null) + return; + + foreach (var toolbarButton in VirtualView.Buttons) + { + var (button, handler) = CreateMaterialIconButton(toolbarButton); + var layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f); + m_buttonsLayout.AddView(button, layoutParams); + m_buttonHandlers.Add((button, handler)); + } + } + + private void UnsubscribeAllButtonHandlers() + { + foreach (var (button, handler) in m_buttonHandlers) + { + button.Click -= handler; + } + m_buttonHandlers.Clear(); + } + + private (MaterialButton Button, EventHandler Handler) CreateMaterialIconButton(ToolbarButton toolbarButton) + { + var iconColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform(); + var iconColorStateList = new ColorStateList( + [[Android.Resource.Attribute.StateEnabled], [-Android.Resource.Attribute.StateEnabled]], + [iconColor, Resources.Colors.Colors.GetColor(ColorName.color_icon_disabled).ToPlatform()]); + + var button = new MaterialButton(Context!, null, Resource.Attribute.materialIconButtonStyle) + { + IconGravity = MaterialButton.IconGravityTextTop, + IconTint = iconColorStateList, + SoundEffectsEnabled = false, + }; + + button.Enabled = toolbarButton.IsEnabled; + + if (!string.IsNullOrEmpty(toolbarButton.Title)) + { + button.ContentDescription = toolbarButton.Title; + } + + if (toolbarButton.Icon is not null && + DUI.TryGetDrawableFromFileImageSource(toolbarButton.Icon, out var drawable) && + drawable is not null) + { + button.Icon = drawable; + } + + void OnClick(object? sender, EventArgs e) => + toolbarButton.Command?.Execute(toolbarButton.CommandParameter); + + button.Click += OnClick; + + return (button, OnClick); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs index 08a9df65e..dec683c57 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs @@ -1,57 +1,15 @@ -using DIPS.Mobile.UI.Components.Buttons; -using DIPS.Mobile.UI.Components.Dividers; -using DIPS.Mobile.UI.Resources.Styles; -using DIPS.Mobile.UI.Resources.Styles.Button; - namespace DIPS.Mobile.UI.Components.Toolbar; /// -/// A cross-platform toolbar component that displays a horizontal bar of icon buttons. +/// A cross-platform toolbar component that displays a horizontal bar of icon buttons using native platform views. /// /// /// iOS: https://developer.apple.com/design/human-interface-guidelines/toolbars /// Android: https://m3.material.io/components/toolbars/overview /// [ContentProperty(nameof(Buttons))] -public partial class Toolbar : ContentView +public partial class Toolbar : View { - private readonly Grid m_buttonsGrid = new(); - - public Toolbar() - { - this.SetAppThemeColor(BackgroundColorProperty, ColorName.color_surface_default); - - var topBorder = new Divider - { - HeightRequest = Sizes.GetSize(SizeName.stroke_small) - }; - - Content = new VerticalStackLayout - { - Spacing = 0, - Children = { topBorder, m_buttonsGrid } - }; - } - - private void OnButtonsChanged() - { - m_buttonsGrid.ColumnDefinitions.Clear(); - m_buttonsGrid.Children.Clear(); - - if (Buttons is null || Buttons.Count == 0) - return; - - for (var i = 0; i < Buttons.Count; i++) - { - m_buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); - var toolbarButton = Buttons[i]; - toolbarButton.BindingContext = BindingContext; - var buttonView = CreateButtonView(toolbarButton); - Grid.SetColumn(buttonView, i); - m_buttonsGrid.Add(buttonView); - } - } - protected override void OnBindingContextChanged() { base.OnBindingContextChanged(); @@ -64,25 +22,4 @@ protected override void OnBindingContextChanged() toolbarButton.BindingContext = BindingContext; } } - - private static View CreateButtonView(ToolbarButton toolbarButton) - { - var button = new Button - { - Style = Styles.GetButtonStyle(ButtonStyle.GhostIconSmall), - HorizontalOptions = LayoutOptions.Center, - }; - - button.SetBinding(Button.ImageSourceProperty, new Binding(nameof(ToolbarButton.Icon), source: toolbarButton)); - button.SetBinding(IsEnabledProperty, new Binding(nameof(ToolbarButton.IsEnabled), source: toolbarButton)); - button.SetBinding(Button.CommandProperty, new Binding(nameof(ToolbarButton.Command), source: toolbarButton)); - button.SetBinding(Button.CommandParameterProperty, new Binding(nameof(ToolbarButton.CommandParameter), source: toolbarButton)); - - if (!string.IsNullOrEmpty(toolbarButton.Title)) - { - SemanticProperties.SetDescription(button, toolbarButton.Title); - } - - return button; - } } diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs new file mode 100644 index 000000000..459b11190 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs @@ -0,0 +1,16 @@ +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class ToolbarHandler +{ + public ToolbarHandler() : base(PropertyMapper) + { + } + + public static readonly IPropertyMapper PropertyMapper = + new PropertyMapper(ViewMapper) + { + [nameof(Toolbar.Buttons)] = MapButtons, + }; + + private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar); +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs new file mode 100644 index 000000000..6224474c1 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs @@ -0,0 +1,12 @@ +using DIPS.Mobile.UI.Exceptions; +using Microsoft.Maui.Handlers; + +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class ToolbarHandler : ViewHandler +{ + protected override object CreatePlatformView() => throw new Only_Here_For_UnitTests(); + + private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) => + throw new Only_Here_For_UnitTests(); +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs new file mode 100644 index 000000000..d659cac1b --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs @@ -0,0 +1,68 @@ +using DIPS.Mobile.UI.API.Library; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using UIKit; + +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class ToolbarHandler : ViewHandler +{ + protected override UIToolbar CreatePlatformView() + { + var toolbar = new UIToolbar(); + toolbar.BarTintColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform(); + toolbar.Translucent = false; + return toolbar; + } + + protected override void ConnectHandler(UIToolbar platformView) + { + base.ConnectHandler(platformView); + UpdateButtons(); + } + + private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) + { + handler.UpdateButtons(); + } + + private void UpdateButtons() + { + var items = new List(); + + // Add a flexible space before the first button to help center/distribute them + items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); + + foreach (var toolbarButton in VirtualView.Buttons) + { + items.Add(CreateBarButtonItem(toolbarButton)); + items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); + } + + PlatformView.SetItems(items.ToArray(), false); + } + + private static UIBarButtonItem CreateBarButtonItem(ToolbarButton toolbarButton) + { + UIImage? icon = null; + if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage)) + { + icon = uiImage?.WithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + } + + var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) => + { + toolbarButton.Command?.Execute(toolbarButton.CommandParameter); + }); + + item.Enabled = toolbarButton.IsEnabled; + item.TintColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform(); + + if (!string.IsNullOrEmpty(toolbarButton.Title)) + { + item.AccessibilityLabel = toolbarButton.Title; + } + + return item; + } +} From 0d08eb631a5c2e900cafa9fdd2a22698cdb360ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:34:51 +0000 Subject: [PATCH 04/13] Align Toolbar with M3 Bottom App Bar specs (80dp height, vertical centering) Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../Components/Toolbar/Android/ToolbarHandler.cs | 5 +++-- .../DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs index f8019228b..b600c6e9d 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs @@ -29,12 +29,13 @@ protected override LinearLayout CreatePlatformView() border.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_border_default).ToPlatform()); outer.AddView(border, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, borderPx)); - // Buttons row + // Buttons row: fills remaining space, centers buttons vertically (M3 spec: icons vertically centered in 80dp bar) m_buttonsLayout = new LinearLayout(Context) { Orientation = Orientation.Horizontal, + Gravity = GravityFlags.CenterVertical, }; - outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent)); + outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)); return outer; } diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs index dec683c57..32946d335 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs @@ -5,11 +5,22 @@ namespace DIPS.Mobile.UI.Components.Toolbar; /// /// /// iOS: https://developer.apple.com/design/human-interface-guidelines/toolbars -/// Android: https://m3.material.io/components/toolbars/overview +/// Android: https://m3.material.io/components/toolbars/overview (Bottom App Bar, height 80dp) /// [ContentProperty(nameof(Buttons))] public partial class Toolbar : View { + public Toolbar() + { + // M3 Bottom App Bar spec: height = 80dp + HeightRequest = Sizes.GetSize(SizeName.size_20); + } + + private void OnButtonsChanged() + { + Handler?.UpdateValue(nameof(Buttons)); + } + protected override void OnBindingContextChanged() { base.OnBindingContextChanged(); From a4318d98b4519c615efb9acee614ff09ebeb5ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Sun, 1 Mar 2026 01:16:06 +0100 Subject: [PATCH 05/13] fixed branch --- .../API/Builder/AppHostBuilderExtensions.cs | 1 + .../Components/Shell/Android/ShellRenderer.cs | 8 ++++---- .../Components/Toolbar/Android/ToolbarHandler.cs | 4 +++- .../Components/Toolbar/iOS/ToolbarHandler.cs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs index 02c0cc710..4200d4e8e 100644 --- a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs +++ b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs @@ -36,6 +36,7 @@ using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar; using SearchBarHandler = DIPS.Mobile.UI.Components.Searching.SearchBarHandler; using ShellRenderer = DIPS.Mobile.UI.Components.Shell.ShellRenderer; +using Toolbar = DIPS.Mobile.UI.Components.Toolbar.Toolbar; namespace DIPS.Mobile.UI.API.Builder; diff --git a/src/library/DIPS.Mobile.UI/Components/Shell/Android/ShellRenderer.cs b/src/library/DIPS.Mobile.UI/Components/Shell/Android/ShellRenderer.cs index f82e536dc..cb2d9ed4e 100644 --- a/src/library/DIPS.Mobile.UI/Components/Shell/Android/ShellRenderer.cs +++ b/src/library/DIPS.Mobile.UI/Components/Shell/Android/ShellRenderer.cs @@ -120,8 +120,8 @@ internal class CustomToolbarAppearanceTracker : ShellToolbarAppearanceTracker { private ShellAppearance? m_appearance; - public Toolbar Toolbar { get; set; } - public override void SetAppearance(Toolbar toolbar, IShellToolbarTracker toolbarTracker, ShellAppearance appearance) + public AndroidX.AppCompat.Widget.Toolbar Toolbar { get; set; } + public override void SetAppearance(AndroidX.AppCompat.Widget.Toolbar toolbar, IShellToolbarTracker toolbarTracker, ShellAppearance appearance) { base.SetAppearance(toolbar, toolbarTracker, appearance); @@ -180,12 +180,12 @@ protected override void Dispose(bool disposing) private class ToolbarMenuItemClickListener : Object, IMenuItemOnMenuItemClickListener, Application.IActivityLifecycleCallbacks, PopupMenu.IOnDismissListener, PopupMenu.IOnMenuItemClickListener { private readonly ContextMenu m_contextMenu; - private readonly Toolbar m_materialToolbar; + private readonly AndroidX.AppCompat.Widget.Toolbar m_materialToolbar; private Dictionary m_menuItems; private PopupMenu? m_popupMenu; private bool m_isShowing; - public ToolbarMenuItemClickListener(ContextMenu contextMenu, Toolbar materialToolbar) + public ToolbarMenuItemClickListener(ContextMenu contextMenu, AndroidX.AppCompat.Widget.Toolbar materialToolbar) { m_contextMenu = contextMenu; m_materialToolbar = materialToolbar; diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs index b600c6e9d..1083135de 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs @@ -5,6 +5,8 @@ using Google.Android.Material.Button; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; +using Orientation = Android.Widget.Orientation; +using View = Android.Views.View; namespace DIPS.Mobile.UI.Components.Toolbar; @@ -33,7 +35,7 @@ protected override LinearLayout CreatePlatformView() m_buttonsLayout = new LinearLayout(Context) { Orientation = Orientation.Horizontal, - Gravity = GravityFlags.CenterVertical, + // Gravity = GravityFlags.CenterVertical, //Didnt work }; outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)); diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs index d659cac1b..378b3ee95 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs @@ -47,7 +47,7 @@ private static UIBarButtonItem CreateBarButtonItem(ToolbarButton toolbarButton) UIImage? icon = null; if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage)) { - icon = uiImage?.WithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + icon = uiImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); } var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) => From 1fc5b71d6d2924767b5b7296bbf36b26e3fa1340 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:49:55 +0000 Subject: [PATCH 06/13] Fix iOS UIToolbar system appearance and Android vertical centering Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../Components/Toolbar/Android/ToolbarHandler.cs | 6 +++--- .../Components/Toolbar/iOS/ToolbarHandler.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs index 1083135de..bbbffb25d 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs @@ -31,13 +31,13 @@ protected override LinearLayout CreatePlatformView() border.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_border_default).ToPlatform()); outer.AddView(border, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, borderPx)); - // Buttons row: fills remaining space, centers buttons vertically (M3 spec: icons vertically centered in 80dp bar) + // Buttons row: fills remaining space with weight=1, centers buttons vertically m_buttonsLayout = new LinearLayout(Context) { Orientation = Orientation.Horizontal, - // Gravity = GravityFlags.CenterVertical, //Didnt work }; - outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)); + m_buttonsLayout.SetGravity(GravityFlags.CenterVertical); + outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, 0, 1f)); return outer; } diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs index 378b3ee95..8695d5342 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs @@ -9,10 +9,10 @@ public partial class ToolbarHandler : ViewHandler { protected override UIToolbar CreatePlatformView() { - var toolbar = new UIToolbar(); - toolbar.BarTintColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform(); - toolbar.Translucent = false; - return toolbar; + // Use the default UIToolbar system appearance — do NOT set BarTintColor or Translucent = false, + // as that overrides the system-provided translucent material (Liquid Glass on iOS 26+, + // blurred translucent bar on earlier versions). + return new UIToolbar(); } protected override void ConnectHandler(UIToolbar platformView) From aeb2b6a7697c11bb151c5bbb69ce4295cfcb31e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:05:05 +0000 Subject: [PATCH 07/13] Override Background mapper to preserve UIToolbar native system appearance Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs index 459b11190..f4f82268c 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs @@ -10,6 +10,10 @@ public ToolbarHandler() : base(PropertyMapper) new PropertyMapper(ViewMapper) { [nameof(Toolbar.Buttons)] = MapButtons, + // Prevent MAUI from overriding the native platform view's own background rendering. + // On iOS, UIToolbar manages its own system appearance (blur/Liquid Glass); + // on Android, we set the background explicitly in CreatePlatformView. + ["Background"] = static (_, _) => { }, }; private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar); From ec06e943aa847c09a920787602877ec09961c66c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:12:54 +0000 Subject: [PATCH 08/13] Explicitly configure UIToolbarAppearance to fix Liquid Glass on standalone UIToolbar Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../Components/Toolbar/iOS/ToolbarHandler.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs index 8695d5342..e4303f9de 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs @@ -7,17 +7,26 @@ namespace DIPS.Mobile.UI.Components.Toolbar; public partial class ToolbarHandler : ViewHandler { - protected override UIToolbar CreatePlatformView() - { - // Use the default UIToolbar system appearance — do NOT set BarTintColor or Translucent = false, - // as that overrides the system-provided translucent material (Liquid Glass on iOS 26+, - // blurred translucent bar on earlier versions). - return new UIToolbar(); - } + protected override UIToolbar CreatePlatformView() => new UIToolbar(); protected override void ConnectHandler(UIToolbar platformView) { base.ConnectHandler(platformView); + + // Explicitly apply the default system appearance so iOS renders the correct + // material: Liquid Glass on iOS 26+, translucent blurred bar on earlier versions. + // ConfigureWithDefaultBackground() is required for standalone toolbars (i.e. not + // managed by UINavigationController) to opt into the platform glass material. + var appearance = new UIToolbarAppearance(); + appearance.ConfigureWithDefaultBackground(); + platformView.StandardAppearance = appearance; + platformView.CompactAppearance = appearance; + platformView.ScrollEdgeAppearance = appearance; + + // Ensure no residual background from MAUI's setup path is blocking the glass layer. + platformView.BackgroundColor = null; + platformView.Layer.BackgroundColor = null; + UpdateButtons(); } From 3952e9be2a9ccf5f7f4b4500075f1178e2315bbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:19:53 +0000 Subject: [PATCH 09/13] Fix iOS toolbar: move 80dp height to Android-only, remove incorrect UIToolbarAppearance config Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../Components/Toolbar/Android/Toolbar.cs | 10 ++++++++++ .../DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs | 9 +++++++-- .../Components/Toolbar/iOS/ToolbarHandler.cs | 15 ++++----------- 3 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs new file mode 100644 index 000000000..5e573609d --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs @@ -0,0 +1,10 @@ +namespace DIPS.Mobile.UI.Components.Toolbar; + +public partial class Toolbar +{ + partial void Init() + { + // M3 Bottom App Bar spec: height = 80dp + HeightRequest = Sizes.GetSize(SizeName.size_20); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs index 32946d335..2e3cd4b6a 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs @@ -12,10 +12,15 @@ public partial class Toolbar : View { public Toolbar() { - // M3 Bottom App Bar spec: height = 80dp - HeightRequest = Sizes.GetSize(SizeName.size_20); + Init(); } + /// + /// Platform-specific initialization. Android sets a HeightRequest per the M3 Bottom App Bar spec (80dp). + /// iOS uses the UIToolbar's intrinsic height. + /// + partial void Init(); + private void OnButtonsChanged() { Handler?.UpdateValue(nameof(Buttons)); diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs index e4303f9de..47891feca 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs @@ -13,17 +13,10 @@ protected override void ConnectHandler(UIToolbar platformView) { base.ConnectHandler(platformView); - // Explicitly apply the default system appearance so iOS renders the correct - // material: Liquid Glass on iOS 26+, translucent blurred bar on earlier versions. - // ConfigureWithDefaultBackground() is required for standalone toolbars (i.e. not - // managed by UINavigationController) to opt into the platform glass material. - var appearance = new UIToolbarAppearance(); - appearance.ConfigureWithDefaultBackground(); - platformView.StandardAppearance = appearance; - platformView.CompactAppearance = appearance; - platformView.ScrollEdgeAppearance = appearance; - - // Ensure no residual background from MAUI's setup path is blocking the glass layer. + // Clear any residual background color that MAUI's setup path may have applied. + // UIToolbar renders its own system material (blur on iOS ≤18, Liquid Glass on iOS 26+) + // via its internal visual effect view; a non-nil BackgroundColor or Layer.BackgroundColor + // would sit on top of and block that rendering. platformView.BackgroundColor = null; platformView.Layer.BackgroundColor = null; From b20a807708eb7359e855d699c663d6b78ad2db3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Sun, 1 Mar 2026 10:01:42 +0100 Subject: [PATCH 10/13] start for toolbar --- .../Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml index 0b7550c48..a1b966cc2 100644 --- a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml @@ -6,13 +6,14 @@ xmlns:local="clr-namespace:Components.ComponentsSamples.Toolbar" x:DataType="local:ToolbarSamplesViewModel" x:Class="Components.ComponentsSamples.Toolbar.ToolbarSamples" - Title="Toolbar"> + Title="Toolbar" + BackgroundColor="White"> - + From a5f6fc1d8088be508f299bca94fea81f8ff7c026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Tue, 3 Mar 2026 22:40:44 +0100 Subject: [PATCH 11/13] changes --- .../Toolbar/ToolbarSamples.xaml | 84 ++---- .../Toolbar/ToolbarSamples.xaml.cs | 5 + .../Components/REGISTER_YOUR_SAMPLES_HERE.cs | 2 +- .../API/Builder/AppHostBuilderExtensions.cs | 3 - .../Components/Pages/Android/ContentPage.cs | 14 + .../Pages/ContentPage.Properties.cs | 19 ++ .../Components/Pages/ContentPage.cs | 15 ++ .../Components/Pages/dotnet/ContentPage.cs | 12 + .../Components/Pages/iOS/ContentPage.cs | 254 ++++++++++++++++++ .../Components/Shell/iOS/ShellRenderer.cs | 1 - .../Components/Toolbar/Android/Toolbar.cs | 10 - .../Toolbar/Android/ToolbarHandler.cs | 123 --------- .../Components/Toolbar/Toolbar.Properties.cs | 23 -- .../Components/Toolbar/Toolbar.cs | 31 ++- .../Components/Toolbar/ToolbarHandler.cs | 20 -- .../Toolbar/dotnet/ToolbarHandler.cs | 12 - .../Components/Toolbar/iOS/ToolbarHandler.cs | 70 ----- 17 files changed, 365 insertions(+), 333 deletions(-) create mode 100644 src/library/DIPS.Mobile.UI/Components/Pages/Android/ContentPage.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Pages/dotnet/ContentPage.cs create mode 100644 src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs delete mode 100644 src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml index a1b966cc2..1929dae20 100644 --- a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml @@ -1,4 +1,6 @@ - + + > - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -78,6 +34,22 @@ Title="Add" Command="{Binding AddCommand}" /> - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs index b6cc63067..0b8a3b249 100644 --- a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml.cs @@ -6,4 +6,9 @@ public ToolbarSamples() { InitializeComponent(); } + + private async void OnCloseClicked(object? sender, EventArgs e) + { + await Navigation.PopModalAsync(); + } } diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs index 4f038643d..e727a727e 100644 --- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs +++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs @@ -74,7 +74,7 @@ public static List RegisterSamples() new(SampleType.Components, "Zoom Container", () => new PanZoomContainerSample()), new(SampleType.Components, "Gallery", () => new GallerySample()), new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()), - new(SampleType.Components, "Toolbar", () => new ToolbarSamples()), + new(SampleType.Components, "Toolbar", () => new ToolbarSamples(), isModal: true), new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()), diff --git a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs index 4200d4e8e..3ba1f0a91 100644 --- a/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs +++ b/src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs @@ -9,7 +9,6 @@ using DIPS.Mobile.UI.Components.Navigation.FloatingNavigationButton; using DIPS.Mobile.UI.Components.PanZoomContainer; using DIPS.Mobile.UI.Components.Pickers.ScrollPicker; -using DIPS.Mobile.UI.Components.Toolbar; using DIPS.Mobile.UI.Effects.Animation.Effects; using DIPS.Mobile.UI.Effects.Touch; using DotNet.Meteor.HotReload.Plugin; @@ -36,7 +35,6 @@ using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar; using SearchBarHandler = DIPS.Mobile.UI.Components.Searching.SearchBarHandler; using ShellRenderer = DIPS.Mobile.UI.Components.Shell.ShellRenderer; -using Toolbar = DIPS.Mobile.UI.Components.Toolbar.Toolbar; namespace DIPS.Mobile.UI.API.Builder; @@ -75,7 +73,6 @@ public static MauiAppBuilder UseDIPSUI( handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); - handlers.AddHandler(); AddPlatformHandlers(handlers); }); diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/Android/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/Android/ContentPage.cs new file mode 100644 index 000000000..8bf8b4973 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Pages/Android/ContentPage.cs @@ -0,0 +1,14 @@ +namespace DIPS.Mobile.UI.Components.Pages; + +public partial class ContentPage +{ + private partial void UpdateBottomToolbarOnPlatform() + { + // TODO: Implement Android bottom toolbar (Material 3 Bottom App Bar) + } + + private partial void HideBottomToolbarOnPlatform() + { + // TODO: Implement Android bottom toolbar cleanup + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.Properties.cs index 8811415a6..fd9a461d6 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.Properties.cs @@ -1,10 +1,29 @@ using DIPS.Mobile.UI.API.Library; +using DIPS.Mobile.UI.Components.Toolbar; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; namespace DIPS.Mobile.UI.Components.Pages; public partial class ContentPage { + public static readonly BindableProperty BottomToolbarProperty = BindableProperty.Create( + nameof(BottomToolbar), + typeof(Toolbar.Toolbar), + typeof(ContentPage)); + + /// + /// A bottom toolbar to display on the page. + /// + /// + /// On iOS, this renders the UINavigationController's built-in toolbar with Liquid Glass on iOS 26+. + /// On Android, a Material 3 Bottom App Bar is displayed at the bottom of the page. + /// + public Toolbar.Toolbar? BottomToolbar + { + get => (Toolbar.Toolbar?)GetValue(BottomToolbarProperty); + set => SetValue(BottomToolbarProperty, value); + } + public static readonly BindableProperty ShouldHideFloatingNavigationMenuProperty = BindableProperty.Create( nameof(ShouldHideFloatingNavigationMenuButton), typeof(bool), diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.cs index b880297d6..9ba08c27f 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pages/ContentPage.cs @@ -73,6 +73,7 @@ protected override void OnAppearing() HasAppeared = true; HideOrShowFloatingNavigationMenu(); + UpdateBottomToolbarOnPlatform(); #if __ANDROID__ // Update status bar color for this page (works for both modal and non-modal) @@ -80,6 +81,9 @@ protected override void OnAppearing() #endif } + private partial void UpdateBottomToolbarOnPlatform(); + private partial void HideBottomToolbarOnPlatform(); + protected override void OnNavigatedTo(NavigatedToEventArgs args) { base.OnNavigatedTo(args); @@ -185,11 +189,22 @@ protected override void OnDisappearing() base.OnDisappearing(); HasAppeared = false; + HideBottomToolbarOnPlatform(); if (Application.Current != null) { Application.Current.RequestedThemeChanged -= OnRequestedThemeChanged; } } + + protected override void OnPropertyChanged(string? propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == nameof(BottomToolbar) && HasAppeared) + { + UpdateBottomToolbarOnPlatform(); + } + } } } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/dotnet/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/dotnet/ContentPage.cs new file mode 100644 index 000000000..fac58a465 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Pages/dotnet/ContentPage.cs @@ -0,0 +1,12 @@ +namespace DIPS.Mobile.UI.Components.Pages; + +public partial class ContentPage +{ + private partial void UpdateBottomToolbarOnPlatform() + { + } + + private partial void HideBottomToolbarOnPlatform() + { + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs new file mode 100644 index 000000000..1651219f2 --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs @@ -0,0 +1,254 @@ +using DIPS.Mobile.UI.API.Library; +using DIPS.Mobile.UI.Resources.Colors; +using CoreGraphics; +using Microsoft.Maui.Platform; +using ObjCRuntime; +using UIKit; +using Colors = DIPS.Mobile.UI.Resources.Colors.Colors; + +namespace DIPS.Mobile.UI.Components.Pages; + +public partial class ContentPage +{ + // We create our own UINavigationController solely to get a system-managed UIToolbar. + // MAUI's NavigationRenderer completely overrides UINavigationController's toolbar mechanism, + // so we cannot use it. By hosting our own nav controller, the toolbar gets Liquid Glass + // on iOS 26+ just like Apple intended — because UIKit manages it. + private UINavigationController? _toolbarNavController; + private UIViewController? _toolbarHostVC; + private NSLayoutConstraint[]? _bottomToolbarConstraints; + + private partial void UpdateBottomToolbarOnPlatform() + { + Dispatcher.Dispatch(UpdateBottomToolbarOnPlatformCore); + } + + private void UpdateBottomToolbarOnPlatformCore() + { + if (Handler?.PlatformView is not UIView pageView) + return; + + if (BottomToolbar is not { } toolbar || toolbar.Buttons.Count == 0) + { + RemoveNativeBottomToolbar(); + return; + } + + var items = new List(); + items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); + + foreach (var toolbarButton in toolbar.Buttons) + { + items.Add(CreateBarButtonItem(toolbarButton)); + items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); + } + + EnsureNativeBottomToolbar(pageView); + _toolbarHostVC!.SetToolbarItems(items.ToArray(), false); + } + + private void EnsureNativeBottomToolbar(UIView pageView) + { + if (_toolbarNavController is not null) + return; + + // Create a dummy VC to host toolbar items on + _toolbarHostVC = new UIViewController(); + _toolbarHostVC.View!.BackgroundColor = UIColor.Clear; + + // Create our own UINavigationController — this gives us a system-managed toolbar + // that gets Liquid Glass on iOS 26+ automatically. + // MAUI's NavigationRenderer blocks the standard approach, so we host our own. + _toolbarNavController = new UINavigationController(_toolbarHostVC); + _toolbarNavController.SetToolbarHidden(false, false); + + // Hide the navigation bar — we only want the toolbar + _toolbarNavController.SetNavigationBarHidden(true, false); + + // Explicitly configure toolbar appearance — use system default so iOS 26 + // renders the glass capsule background (not transparent or opaque overrides) + var toolbar = _toolbarNavController.Toolbar; + toolbar.Translucent = true; + var toolbarAppearance = new UIToolbarAppearance(); + toolbarAppearance.ConfigureWithDefaultBackground(); + toolbar.StandardAppearance = toolbarAppearance; + toolbar.ScrollEdgeAppearance = toolbarAppearance; + if (toolbar.RespondsToSelector(new Selector("compactScrollEdgeAppearance"))) + { + toolbar.CompactScrollEdgeAppearance = toolbarAppearance; + } + + _toolbarNavController.View!.TranslatesAutoresizingMaskIntoConstraints = false; + + // Add to the parent VC as a child view controller for proper containment. + // This is important — the system needs proper VC containment to apply Liquid Glass. + var parentVC = FindViewController(); + if (parentVC is not null) + { + parentVC.AddChildViewController(_toolbarNavController); + parentVC.View!.AddSubview(_toolbarNavController.View); + _toolbarNavController.DidMoveToParentViewController(parentVC); + } + else + { + // Fallback: add to window + var window = pageView.Window ?? UIApplication.SharedApplication.KeyWindow; + var targetView = window ?? pageView; + targetView.AddSubview(_toolbarNavController.View); + } + + // Pin the nav controller's view to all edges of the parent. + // Let the system manage the toolbar's position and size naturally. + // The toolbar will sit at the bottom; the rest of the view area above it + // is the host VC's view (transparent, so content shows through). + var containerView = _toolbarNavController.View.Superview!; + _bottomToolbarConstraints = + [ + _toolbarNavController.View.LeadingAnchor.ConstraintEqualTo(containerView.LeadingAnchor), + _toolbarNavController.View.TrailingAnchor.ConstraintEqualTo(containerView.TrailingAnchor), + _toolbarNavController.View.BottomAnchor.ConstraintEqualTo(containerView.BottomAnchor), + _toolbarNavController.View.TopAnchor.ConstraintEqualTo(containerView.TopAnchor) + ]; + NSLayoutConstraint.ActivateConstraints(_bottomToolbarConstraints); + + // DEBUG: Dump toolbar diagnostics after layout + _toolbarNavController.View.LayoutIfNeeded(); + DumpToolbarDiagnostics(); + } + + private void DumpToolbarDiagnostics() + { + if (_toolbarNavController is null) return; + + var toolbar = _toolbarNavController.Toolbar; + var info = $"=== TOOLBAR DIAGNOSTICS ===\n"; + info += $"Toolbar frame: {toolbar.Frame}\n"; + info += $"Toolbar bounds: {toolbar.Bounds}\n"; + info += $"Toolbar translucent: {toolbar.Translucent}\n"; + info += $"Toolbar hidden: {toolbar.Hidden}\n"; + info += $"Toolbar alpha: {toolbar.Alpha}\n"; + info += $"Toolbar barStyle: {toolbar.BarStyle}\n"; + info += $"Toolbar barTintColor: {toolbar.BarTintColor}\n"; + info += $"Toolbar backgroundColor: {toolbar.BackgroundColor}\n"; + info += $"Toolbar clipsToBounds: {toolbar.ClipsToBounds}\n"; + info += $"Toolbar items count: {toolbar.Items?.Length ?? 0}\n"; + info += $"NavController view frame: {_toolbarNavController.View!.Frame}\n"; + info += $"HostVC view frame: {_toolbarHostVC?.View?.Frame}\n"; + info += $"NavController view userInteraction: {_toolbarNavController.View.UserInteractionEnabled}\n"; + info += $"Toolbar userInteraction: {toolbar.UserInteractionEnabled}\n"; + + // Check for glass-related selectors + var glassSelectors = new[] + { + "preferredGlassEffect", "glassEffect", "_glassEffect", + "setPreferredGlassEffect:", "_setGlassEffect:", + "glassContainerView", "_backgroundEffectView", + "preferredBarStyle", "_effectiveGlassEffect", + "backgroundEffect", "setBackgroundEffect:", + "_liquidGlassFrame", "_liquidGlassStyle", + "preferredBackgroundStyle", "setPreferredBackgroundStyle:", + "_backdropStyle", "setBackdropStyle:", + }; + + info += "\n=== GLASS SELECTORS ===\n"; + foreach (var sel in glassSelectors) + { + var responds = toolbar.RespondsToSelector(new Selector(sel)); + if (responds) + info += $" ✅ {sel}\n"; + } + + // Dump toolbar subview hierarchy + info += "\n=== TOOLBAR SUBVIEWS ===\n"; + info += DumpViewHierarchy(toolbar, 0); + + // Also dump navcontroller.toolbar parent + info += $"\n=== TOOLBAR SUPERVIEW ===\n"; + info += $"SuperView: {toolbar.Superview?.GetType().Name} frame={toolbar.Superview?.Frame}\n"; + + System.Diagnostics.Debug.WriteLine(info); + Console.WriteLine(info); + + // Also show as alert for easy reading on device + var alert = UIAlertController.Create("Toolbar Info", info, UIAlertControllerStyle.Alert); + alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null)); + _toolbarNavController.PresentViewController(alert, true, null); + } + + private static string DumpViewHierarchy(UIView view, int depth) + { + var indent = new string(' ', depth * 2); + var result = $"{indent}{view.GetType().Name} (ObjC: {view.Class.Name}) frame={view.Frame} alpha={view.Alpha} hidden={view.Hidden} clips={view.ClipsToBounds}\n"; + foreach (var subview in view.Subviews) + { + result += DumpViewHierarchy(subview, depth + 1); + } + return result; + } + + private void RemoveNativeBottomToolbar() + { + if (_toolbarNavController is null) + return; + + if (_bottomToolbarConstraints is not null) + { + NSLayoutConstraint.DeactivateConstraints(_bottomToolbarConstraints); + _bottomToolbarConstraints = null; + } + + _toolbarNavController.WillMoveToParentViewController(null); + _toolbarNavController.View?.RemoveFromSuperview(); + _toolbarNavController.RemoveFromParentViewController(); + _toolbarNavController.Dispose(); + _toolbarNavController = null; + + _toolbarHostVC?.Dispose(); + _toolbarHostVC = null; + } + + private partial void HideBottomToolbarOnPlatform() + { + RemoveNativeBottomToolbar(); + } + + private UIViewController? FindViewController() + { + if (Handler?.PlatformView is not UIView platformView) + return null; + + var responder = platformView.NextResponder; + while (responder is not null) + { + if (responder is UIViewController vc) + return vc; + responder = responder.NextResponder; + } + + return null; + } + + private static UIBarButtonItem CreateBarButtonItem(Toolbar.ToolbarButton toolbarButton) + { + UIImage? icon = null; + if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage)) + { + icon = uiImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + } + + var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) => + { + toolbarButton.Command?.Execute(toolbarButton.CommandParameter); + }); + + item.Enabled = toolbarButton.IsEnabled; + item.TintColor = Colors.GetColor(ColorName.color_icon_action).ToPlatform(); + + if (!string.IsNullOrEmpty(toolbarButton.Title)) + { + item.AccessibilityLabel = toolbarButton.Title; + } + + return item; + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/Shell/iOS/ShellRenderer.cs b/src/library/DIPS.Mobile.UI/Components/Shell/iOS/ShellRenderer.cs index bf4064e68..38f4303ff 100644 --- a/src/library/DIPS.Mobile.UI/Components/Shell/iOS/ShellRenderer.cs +++ b/src/library/DIPS.Mobile.UI/Components/Shell/iOS/ShellRenderer.cs @@ -6,7 +6,6 @@ using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; using Microsoft.Maui.Platform; using UIKit; -using Colors = DIPS.Mobile.UI.Resources.Colors.Colors; using ContentPage = DIPS.Mobile.UI.Components.Pages.ContentPage; using Page = Microsoft.Maui.Controls.Page; diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs deleted file mode 100644 index 5e573609d..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/Toolbar.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class Toolbar -{ - partial void Init() - { - // M3 Bottom App Bar spec: height = 80dp - HeightRequest = Sizes.GetSize(SizeName.size_20); - } -} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs deleted file mode 100644 index bbbffb25d..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Android/ToolbarHandler.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Android.Content.Res; -using Android.Views; -using Android.Widget; -using DIPS.Mobile.UI.API.Library; -using Google.Android.Material.Button; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform; -using Orientation = Android.Widget.Orientation; -using View = Android.Views.View; - -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class ToolbarHandler : ViewHandler -{ - private LinearLayout? m_buttonsLayout; - private readonly List<(MaterialButton Button, EventHandler Handler)> m_buttonHandlers = []; - - protected override LinearLayout CreatePlatformView() - { - // Outer vertical layout: [top border row] + [buttons row] - var outer = new LinearLayout(Context) - { - Orientation = Orientation.Vertical, - }; - outer.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform()); - - // Top border - var density = Context?.Resources?.DisplayMetrics?.Density ?? 1f; - var borderPx = (int)(Sizes.GetSize(SizeName.stroke_small) * density); - var border = new View(Context); - border.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_border_default).ToPlatform()); - outer.AddView(border, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, borderPx)); - - // Buttons row: fills remaining space with weight=1, centers buttons vertically - m_buttonsLayout = new LinearLayout(Context) - { - Orientation = Orientation.Horizontal, - }; - m_buttonsLayout.SetGravity(GravityFlags.CenterVertical); - outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, 0, 1f)); - - return outer; - } - - protected override void ConnectHandler(LinearLayout platformView) - { - base.ConnectHandler(platformView); - UpdateButtons(); - } - - protected override void DisconnectHandler(LinearLayout platformView) - { - UnsubscribeAllButtonHandlers(); - base.DisconnectHandler(platformView); - } - - private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) - { - handler.UpdateButtons(); - } - - private void UpdateButtons() - { - UnsubscribeAllButtonHandlers(); - m_buttonsLayout?.RemoveAllViews(); - - if (VirtualView.Buttons is null || m_buttonsLayout is null) - return; - - foreach (var toolbarButton in VirtualView.Buttons) - { - var (button, handler) = CreateMaterialIconButton(toolbarButton); - var layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f); - m_buttonsLayout.AddView(button, layoutParams); - m_buttonHandlers.Add((button, handler)); - } - } - - private void UnsubscribeAllButtonHandlers() - { - foreach (var (button, handler) in m_buttonHandlers) - { - button.Click -= handler; - } - m_buttonHandlers.Clear(); - } - - private (MaterialButton Button, EventHandler Handler) CreateMaterialIconButton(ToolbarButton toolbarButton) - { - var iconColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform(); - var iconColorStateList = new ColorStateList( - [[Android.Resource.Attribute.StateEnabled], [-Android.Resource.Attribute.StateEnabled]], - [iconColor, Resources.Colors.Colors.GetColor(ColorName.color_icon_disabled).ToPlatform()]); - - var button = new MaterialButton(Context!, null, Resource.Attribute.materialIconButtonStyle) - { - IconGravity = MaterialButton.IconGravityTextTop, - IconTint = iconColorStateList, - SoundEffectsEnabled = false, - }; - - button.Enabled = toolbarButton.IsEnabled; - - if (!string.IsNullOrEmpty(toolbarButton.Title)) - { - button.ContentDescription = toolbarButton.Title; - } - - if (toolbarButton.Icon is not null && - DUI.TryGetDrawableFromFileImageSource(toolbarButton.Icon, out var drawable) && - drawable is not null) - { - button.Icon = drawable; - } - - void OnClick(object? sender, EventArgs e) => - toolbarButton.Command?.Execute(toolbarButton.CommandParameter); - - button.Click += OnClick; - - return (button, OnClick); - } -} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs deleted file mode 100644 index 7456a7bb6..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.Properties.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class Toolbar -{ - /// - /// - /// - public static readonly BindableProperty ButtonsProperty = BindableProperty.Create( - nameof(Buttons), - typeof(IList), - typeof(Toolbar), - defaultValueCreator: _ => new List(), - propertyChanged: (bindable, _, _) => ((Toolbar)bindable).OnButtonsChanged()); - - /// - /// The buttons to display in the toolbar. - /// - public IList Buttons - { - get => (IList)GetValue(ButtonsProperty); - set => SetValue(ButtonsProperty, value); - } -} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs index 2e3cd4b6a..9b920f26c 100644 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs +++ b/src/library/DIPS.Mobile.UI/Components/Toolbar/Toolbar.cs @@ -1,29 +1,32 @@ +using System.Collections.ObjectModel; + namespace DIPS.Mobile.UI.Components.Toolbar; /// -/// A cross-platform toolbar component that displays a horizontal bar of icon buttons using native platform views. +/// A cross-platform toolbar that displays a horizontal bar of icon buttons. +/// Set this on to display a bottom toolbar. /// /// -/// iOS: https://developer.apple.com/design/human-interface-guidelines/toolbars -/// Android: https://m3.material.io/components/toolbars/overview (Bottom App Bar, height 80dp) +/// On iOS the toolbar is rendered by the UINavigationController's built-in bottom toolbar, +/// which provides native Liquid Glass on iOS 26+. +/// On Android a Material 3 Bottom App Bar is injected at the bottom of the page. /// [ContentProperty(nameof(Buttons))] -public partial class Toolbar : View +public class Toolbar : Element { - public Toolbar() - { - Init(); - } + public static readonly BindableProperty ButtonsProperty = BindableProperty.Create( + nameof(Buttons), + typeof(IList), + typeof(Toolbar), + defaultValueCreator: _ => new ObservableCollection()); /// - /// Platform-specific initialization. Android sets a HeightRequest per the M3 Bottom App Bar spec (80dp). - /// iOS uses the UIToolbar's intrinsic height. + /// The buttons displayed in the toolbar. /// - partial void Init(); - - private void OnButtonsChanged() + public IList Buttons { - Handler?.UpdateValue(nameof(Buttons)); + get => (IList)GetValue(ButtonsProperty); + set => SetValue(ButtonsProperty, value); } protected override void OnBindingContextChanged() diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs deleted file mode 100644 index f4f82268c..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/ToolbarHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class ToolbarHandler -{ - public ToolbarHandler() : base(PropertyMapper) - { - } - - public static readonly IPropertyMapper PropertyMapper = - new PropertyMapper(ViewMapper) - { - [nameof(Toolbar.Buttons)] = MapButtons, - // Prevent MAUI from overriding the native platform view's own background rendering. - // On iOS, UIToolbar manages its own system appearance (blur/Liquid Glass); - // on Android, we set the background explicitly in CreatePlatformView. - ["Background"] = static (_, _) => { }, - }; - - private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar); -} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs deleted file mode 100644 index 6224474c1..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/dotnet/ToolbarHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using DIPS.Mobile.UI.Exceptions; -using Microsoft.Maui.Handlers; - -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class ToolbarHandler : ViewHandler -{ - protected override object CreatePlatformView() => throw new Only_Here_For_UnitTests(); - - private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) => - throw new Only_Here_For_UnitTests(); -} diff --git a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs deleted file mode 100644 index 47891feca..000000000 --- a/src/library/DIPS.Mobile.UI/Components/Toolbar/iOS/ToolbarHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -using DIPS.Mobile.UI.API.Library; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform; -using UIKit; - -namespace DIPS.Mobile.UI.Components.Toolbar; - -public partial class ToolbarHandler : ViewHandler -{ - protected override UIToolbar CreatePlatformView() => new UIToolbar(); - - protected override void ConnectHandler(UIToolbar platformView) - { - base.ConnectHandler(platformView); - - // Clear any residual background color that MAUI's setup path may have applied. - // UIToolbar renders its own system material (blur on iOS ≤18, Liquid Glass on iOS 26+) - // via its internal visual effect view; a non-nil BackgroundColor or Layer.BackgroundColor - // would sit on top of and block that rendering. - platformView.BackgroundColor = null; - platformView.Layer.BackgroundColor = null; - - UpdateButtons(); - } - - private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) - { - handler.UpdateButtons(); - } - - private void UpdateButtons() - { - var items = new List(); - - // Add a flexible space before the first button to help center/distribute them - items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); - - foreach (var toolbarButton in VirtualView.Buttons) - { - items.Add(CreateBarButtonItem(toolbarButton)); - items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace)); - } - - PlatformView.SetItems(items.ToArray(), false); - } - - private static UIBarButtonItem CreateBarButtonItem(ToolbarButton toolbarButton) - { - UIImage? icon = null; - if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage)) - { - icon = uiImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); - } - - var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) => - { - toolbarButton.Command?.Execute(toolbarButton.CommandParameter); - }); - - item.Enabled = toolbarButton.IsEnabled; - item.TintColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform(); - - if (!string.IsNullOrEmpty(toolbarButton.Title)) - { - item.AccessibilityLabel = toolbarButton.Title; - } - - return item; - } -} From 0fd231e751c37e7d54eafd416ff8bb00f3fa8f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Tue, 3 Mar 2026 23:15:18 +0100 Subject: [PATCH 12/13] we got something --- .../Toolbar/ToolbarSamples.xaml | 22 +- .../Components/Pages/iOS/ContentPage.cs | 214 +++++++----------- 2 files changed, 103 insertions(+), 133 deletions(-) diff --git a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml index 1929dae20..3e2884b84 100644 --- a/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml +++ b/src/app/Components/ComponentsSamples/Toolbar/ToolbarSamples.xaml @@ -37,11 +37,11 @@ - + + + + + + + + + + + + + + + + + diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs index 1651219f2..89d3f9998 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs @@ -1,6 +1,9 @@ +using System.Runtime.InteropServices; +using CoreAnimation; using DIPS.Mobile.UI.API.Library; using DIPS.Mobile.UI.Resources.Colors; using CoreGraphics; +using Foundation; using Microsoft.Maui.Platform; using ObjCRuntime; using UIKit; @@ -10,12 +13,16 @@ namespace DIPS.Mobile.UI.Components.Pages; public partial class ContentPage { - // We create our own UINavigationController solely to get a system-managed UIToolbar. - // MAUI's NavigationRenderer completely overrides UINavigationController's toolbar mechanism, - // so we cannot use it. By hosting our own nav controller, the toolbar gets Liquid Glass - // on iOS 26+ just like Apple intended — because UIKit manages it. - private UINavigationController? _toolbarNavController; - private UIViewController? _toolbarHostVC; + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] + private static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector); + + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] + private static extern void objc_msgSend_void_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg); + + // The glass capsule container that holds the toolbar + private UIView? _capsuleContainer; + private UIVisualEffectView? _glassEffectView; + private UIToolbar? _bottomToolbar; private NSLayoutConstraint[]? _bottomToolbarConstraints; private partial void UpdateBottomToolbarOnPlatform() @@ -44,151 +51,93 @@ private void UpdateBottomToolbarOnPlatformCore() } EnsureNativeBottomToolbar(pageView); - _toolbarHostVC!.SetToolbarItems(items.ToArray(), false); + _bottomToolbar!.SetItems(items.ToArray(), false); } private void EnsureNativeBottomToolbar(UIView pageView) { - if (_toolbarNavController is not null) + if (_capsuleContainer is not null) return; - // Create a dummy VC to host toolbar items on - _toolbarHostVC = new UIViewController(); - _toolbarHostVC.View!.BackgroundColor = UIColor.Clear; - - // Create our own UINavigationController — this gives us a system-managed toolbar - // that gets Liquid Glass on iOS 26+ automatically. - // MAUI's NavigationRenderer blocks the standard approach, so we host our own. - _toolbarNavController = new UINavigationController(_toolbarHostVC); - _toolbarNavController.SetToolbarHidden(false, false); - - // Hide the navigation bar — we only want the toolbar - _toolbarNavController.SetNavigationBarHidden(true, false); - - // Explicitly configure toolbar appearance — use system default so iOS 26 - // renders the glass capsule background (not transparent or opaque overrides) - var toolbar = _toolbarNavController.Toolbar; - toolbar.Translucent = true; - var toolbarAppearance = new UIToolbarAppearance(); - toolbarAppearance.ConfigureWithDefaultBackground(); - toolbar.StandardAppearance = toolbarAppearance; - toolbar.ScrollEdgeAppearance = toolbarAppearance; - if (toolbar.RespondsToSelector(new Selector("compactScrollEdgeAppearance"))) - { - toolbar.CompactScrollEdgeAppearance = toolbarAppearance; - } - - _toolbarNavController.View!.TranslatesAutoresizingMaskIntoConstraints = false; - - // Add to the parent VC as a child view controller for proper containment. - // This is important — the system needs proper VC containment to apply Liquid Glass. var parentVC = FindViewController(); - if (parentVC is not null) + var containerView = parentVC?.View ?? pageView; + + // 1. Create the capsule container — a rounded rect that clips its children + _capsuleContainer = new UIView(); + _capsuleContainer.TranslatesAutoresizingMaskIntoConstraints = false; + _capsuleContainer.ClipsToBounds = false; // Don't clip — allows button press animations to overflow + _capsuleContainer.Layer.CornerRadius = 26; + _capsuleContainer.Layer.CornerCurve = CoreAnimation.CACornerCurve.Continuous; + containerView.AddSubview(_capsuleContainer); + + // 2. Create the glass background using UIVisualEffectView + UIGlassEffect + var glassEffect = TryCreateGlassEffect(); + if (glassEffect is not null) { - parentVC.AddChildViewController(_toolbarNavController); - parentVC.View!.AddSubview(_toolbarNavController.View); - _toolbarNavController.DidMoveToParentViewController(parentVC); + _glassEffectView = new UIVisualEffectView(glassEffect); } else { - // Fallback: add to window - var window = pageView.Window ?? UIApplication.SharedApplication.KeyWindow; - var targetView = window ?? pageView; - targetView.AddSubview(_toolbarNavController.View); + // Fallback for pre-iOS 26: system thin material blur + _glassEffectView = new UIVisualEffectView(UIBlurEffect.FromStyle(UIBlurEffectStyle.SystemThinMaterial)); } - - // Pin the nav controller's view to all edges of the parent. - // Let the system manage the toolbar's position and size naturally. - // The toolbar will sit at the bottom; the rest of the view area above it - // is the host VC's view (transparent, so content shows through). - var containerView = _toolbarNavController.View.Superview!; + _glassEffectView.TranslatesAutoresizingMaskIntoConstraints = false; + _capsuleContainer.AddSubview(_glassEffectView); + + // 3. Create the toolbar with transparent background inside the capsule + _bottomToolbar = new UIToolbar(); + _bottomToolbar.TranslatesAutoresizingMaskIntoConstraints = false; + _bottomToolbar.SetBackgroundImage(new UIImage(), UIToolbarPosition.Any, UIBarMetrics.Default); + _bottomToolbar.SetShadowImage(new UIImage(), UIToolbarPosition.Any); + _bottomToolbar.BackgroundColor = UIColor.Clear; + _capsuleContainer.AddSubview(_bottomToolbar); + + // Layout constraints + var safeArea = containerView.SafeAreaLayoutGuide; _bottomToolbarConstraints = [ - _toolbarNavController.View.LeadingAnchor.ConstraintEqualTo(containerView.LeadingAnchor), - _toolbarNavController.View.TrailingAnchor.ConstraintEqualTo(containerView.TrailingAnchor), - _toolbarNavController.View.BottomAnchor.ConstraintEqualTo(containerView.BottomAnchor), - _toolbarNavController.View.TopAnchor.ConstraintEqualTo(containerView.TopAnchor) + // Capsule container: horizontal insets + pinned to safe area bottom + _capsuleContainer.LeadingAnchor.ConstraintEqualTo(safeArea.LeadingAnchor, 48), + _capsuleContainer.TrailingAnchor.ConstraintEqualTo(safeArea.TrailingAnchor, -48), + _capsuleContainer.BottomAnchor.ConstraintEqualTo(safeArea.BottomAnchor, -8), + _capsuleContainer.HeightAnchor.ConstraintEqualTo(52), + + // Glass effect view fills capsule + _glassEffectView.LeadingAnchor.ConstraintEqualTo(_capsuleContainer.LeadingAnchor), + _glassEffectView.TrailingAnchor.ConstraintEqualTo(_capsuleContainer.TrailingAnchor), + _glassEffectView.TopAnchor.ConstraintEqualTo(_capsuleContainer.TopAnchor), + _glassEffectView.BottomAnchor.ConstraintEqualTo(_capsuleContainer.BottomAnchor), + + // Toolbar fills capsule + _bottomToolbar.LeadingAnchor.ConstraintEqualTo(_capsuleContainer.LeadingAnchor), + _bottomToolbar.TrailingAnchor.ConstraintEqualTo(_capsuleContainer.TrailingAnchor), + _bottomToolbar.TopAnchor.ConstraintEqualTo(_capsuleContainer.TopAnchor), + _bottomToolbar.BottomAnchor.ConstraintEqualTo(_capsuleContainer.BottomAnchor), ]; NSLayoutConstraint.ActivateConstraints(_bottomToolbarConstraints); - // DEBUG: Dump toolbar diagnostics after layout - _toolbarNavController.View.LayoutIfNeeded(); - DumpToolbarDiagnostics(); + containerView.SetNeedsLayout(); + containerView.LayoutIfNeeded(); } - private void DumpToolbarDiagnostics() + private UIVisualEffect? TryCreateGlassEffect() { - if (_toolbarNavController is null) return; - - var toolbar = _toolbarNavController.Toolbar; - var info = $"=== TOOLBAR DIAGNOSTICS ===\n"; - info += $"Toolbar frame: {toolbar.Frame}\n"; - info += $"Toolbar bounds: {toolbar.Bounds}\n"; - info += $"Toolbar translucent: {toolbar.Translucent}\n"; - info += $"Toolbar hidden: {toolbar.Hidden}\n"; - info += $"Toolbar alpha: {toolbar.Alpha}\n"; - info += $"Toolbar barStyle: {toolbar.BarStyle}\n"; - info += $"Toolbar barTintColor: {toolbar.BarTintColor}\n"; - info += $"Toolbar backgroundColor: {toolbar.BackgroundColor}\n"; - info += $"Toolbar clipsToBounds: {toolbar.ClipsToBounds}\n"; - info += $"Toolbar items count: {toolbar.Items?.Length ?? 0}\n"; - info += $"NavController view frame: {_toolbarNavController.View!.Frame}\n"; - info += $"HostVC view frame: {_toolbarHostVC?.View?.Frame}\n"; - info += $"NavController view userInteraction: {_toolbarNavController.View.UserInteractionEnabled}\n"; - info += $"Toolbar userInteraction: {toolbar.UserInteractionEnabled}\n"; - - // Check for glass-related selectors - var glassSelectors = new[] - { - "preferredGlassEffect", "glassEffect", "_glassEffect", - "setPreferredGlassEffect:", "_setGlassEffect:", - "glassContainerView", "_backgroundEffectView", - "preferredBarStyle", "_effectiveGlassEffect", - "backgroundEffect", "setBackgroundEffect:", - "_liquidGlassFrame", "_liquidGlassStyle", - "preferredBackgroundStyle", "setPreferredBackgroundStyle:", - "_backdropStyle", "setBackdropStyle:", - }; - - info += "\n=== GLASS SELECTORS ===\n"; - foreach (var sel in glassSelectors) - { - var responds = toolbar.RespondsToSelector(new Selector(sel)); - if (responds) - info += $" ✅ {sel}\n"; - } - - // Dump toolbar subview hierarchy - info += "\n=== TOOLBAR SUBVIEWS ===\n"; - info += DumpViewHierarchy(toolbar, 0); + // On iOS 26+, UIGlassEffect is a UIVisualEffect subclass + var glassEffectClass = Class.GetHandle("UIGlassEffect"); + if (glassEffectClass == IntPtr.Zero) return null; - // Also dump navcontroller.toolbar parent - info += $"\n=== TOOLBAR SUPERVIEW ===\n"; - info += $"SuperView: {toolbar.Superview?.GetType().Name} frame={toolbar.Superview?.Frame}\n"; + var alloced = objc_msgSend_IntPtr(glassEffectClass, Selector.GetHandle("alloc")); + if (alloced == IntPtr.Zero) return null; - System.Diagnostics.Debug.WriteLine(info); - Console.WriteLine(info); - - // Also show as alert for easy reading on device - var alert = UIAlertController.Create("Toolbar Info", info, UIAlertControllerStyle.Alert); - alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null)); - _toolbarNavController.PresentViewController(alert, true, null); - } + var instance = objc_msgSend_IntPtr(alloced, Selector.GetHandle("init")); + if (instance == IntPtr.Zero) return null; - private static string DumpViewHierarchy(UIView view, int depth) - { - var indent = new string(' ', depth * 2); - var result = $"{indent}{view.GetType().Name} (ObjC: {view.Class.Name}) frame={view.Frame} alpha={view.Alpha} hidden={view.Hidden} clips={view.ClipsToBounds}\n"; - foreach (var subview in view.Subviews) - { - result += DumpViewHierarchy(subview, depth + 1); - } - return result; + return Runtime.GetNSObject(instance); } private void RemoveNativeBottomToolbar() { - if (_toolbarNavController is null) + if (_capsuleContainer is null) return; if (_bottomToolbarConstraints is not null) @@ -197,14 +146,17 @@ private void RemoveNativeBottomToolbar() _bottomToolbarConstraints = null; } - _toolbarNavController.WillMoveToParentViewController(null); - _toolbarNavController.View?.RemoveFromSuperview(); - _toolbarNavController.RemoveFromParentViewController(); - _toolbarNavController.Dispose(); - _toolbarNavController = null; + _bottomToolbar?.RemoveFromSuperview(); + _bottomToolbar?.Dispose(); + _bottomToolbar = null; + + _glassEffectView?.RemoveFromSuperview(); + _glassEffectView?.Dispose(); + _glassEffectView = null; - _toolbarHostVC?.Dispose(); - _toolbarHostVC = null; + _capsuleContainer.RemoveFromSuperview(); + _capsuleContainer.Dispose(); + _capsuleContainer = null; } private partial void HideBottomToolbarOnPlatform() From 59dca1da973a89c80226fe365cb20d0ba929b77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Wed, 4 Mar 2026 07:51:43 +0100 Subject: [PATCH 13/13] fixed --- .../Components/Pages/iOS/ContentPage.cs | 57 +++++++------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs index 89d3f9998..e31c3af3b 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pages/iOS/ContentPage.cs @@ -1,11 +1,8 @@ -using System.Runtime.InteropServices; using CoreAnimation; using DIPS.Mobile.UI.API.Library; using DIPS.Mobile.UI.Resources.Colors; using CoreGraphics; -using Foundation; using Microsoft.Maui.Platform; -using ObjCRuntime; using UIKit; using Colors = DIPS.Mobile.UI.Resources.Colors.Colors; @@ -13,12 +10,6 @@ namespace DIPS.Mobile.UI.Components.Pages; public partial class ContentPage { - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector); - - [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] - private static extern void objc_msgSend_void_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg); - // The glass capsule container that holds the toolbar private UIView? _capsuleContainer; private UIVisualEffectView? _glassEffectView; @@ -66,15 +57,14 @@ private void EnsureNativeBottomToolbar(UIView pageView) _capsuleContainer = new UIView(); _capsuleContainer.TranslatesAutoresizingMaskIntoConstraints = false; _capsuleContainer.ClipsToBounds = false; // Don't clip — allows button press animations to overflow - _capsuleContainer.Layer.CornerRadius = 26; + _capsuleContainer.Layer.CornerRadius = 30; // Half of 60pt height = true pill shape _capsuleContainer.Layer.CornerCurve = CoreAnimation.CACornerCurve.Continuous; containerView.AddSubview(_capsuleContainer); // 2. Create the glass background using UIVisualEffectView + UIGlassEffect - var glassEffect = TryCreateGlassEffect(); - if (glassEffect is not null) + if (OperatingSystem.IsIOSVersionAtLeast(26)) { - _glassEffectView = new UIVisualEffectView(glassEffect); + _glassEffectView = new UIVisualEffectView(new UIGlassEffect()); } else { @@ -82,6 +72,9 @@ private void EnsureNativeBottomToolbar(UIView pageView) _glassEffectView = new UIVisualEffectView(UIBlurEffect.FromStyle(UIBlurEffectStyle.SystemThinMaterial)); } _glassEffectView.TranslatesAutoresizingMaskIntoConstraints = false; + _glassEffectView.ClipsToBounds = true; + _glassEffectView.Layer.CornerRadius = 30; // Match capsule + _glassEffectView.Layer.CornerCurve = CACornerCurve.Continuous; _capsuleContainer.AddSubview(_glassEffectView); // 3. Create the toolbar with transparent background inside the capsule @@ -100,7 +93,7 @@ private void EnsureNativeBottomToolbar(UIView pageView) _capsuleContainer.LeadingAnchor.ConstraintEqualTo(safeArea.LeadingAnchor, 48), _capsuleContainer.TrailingAnchor.ConstraintEqualTo(safeArea.TrailingAnchor, -48), _capsuleContainer.BottomAnchor.ConstraintEqualTo(safeArea.BottomAnchor, -8), - _capsuleContainer.HeightAnchor.ConstraintEqualTo(52), + _capsuleContainer.HeightAnchor.ConstraintEqualTo(60), // Glass effect view fills capsule _glassEffectView.LeadingAnchor.ConstraintEqualTo(_capsuleContainer.LeadingAnchor), @@ -108,11 +101,11 @@ private void EnsureNativeBottomToolbar(UIView pageView) _glassEffectView.TopAnchor.ConstraintEqualTo(_capsuleContainer.TopAnchor), _glassEffectView.BottomAnchor.ConstraintEqualTo(_capsuleContainer.BottomAnchor), - // Toolbar fills capsule - _bottomToolbar.LeadingAnchor.ConstraintEqualTo(_capsuleContainer.LeadingAnchor), - _bottomToolbar.TrailingAnchor.ConstraintEqualTo(_capsuleContainer.TrailingAnchor), - _bottomToolbar.TopAnchor.ConstraintEqualTo(_capsuleContainer.TopAnchor), - _bottomToolbar.BottomAnchor.ConstraintEqualTo(_capsuleContainer.BottomAnchor), + // Toolbar inset inside capsule for horizontal padding, vertically centered + _bottomToolbar.LeadingAnchor.ConstraintEqualTo(_capsuleContainer.LeadingAnchor, 8), + _bottomToolbar.TrailingAnchor.ConstraintEqualTo(_capsuleContainer.TrailingAnchor, -8), + _bottomToolbar.CenterYAnchor.ConstraintEqualTo(_capsuleContainer.CenterYAnchor), + _bottomToolbar.HeightAnchor.ConstraintEqualTo(44), ]; NSLayoutConstraint.ActivateConstraints(_bottomToolbarConstraints); @@ -120,21 +113,6 @@ private void EnsureNativeBottomToolbar(UIView pageView) containerView.LayoutIfNeeded(); } - private UIVisualEffect? TryCreateGlassEffect() - { - // On iOS 26+, UIGlassEffect is a UIVisualEffect subclass - var glassEffectClass = Class.GetHandle("UIGlassEffect"); - if (glassEffectClass == IntPtr.Zero) return null; - - var alloced = objc_msgSend_IntPtr(glassEffectClass, Selector.GetHandle("alloc")); - if (alloced == IntPtr.Zero) return null; - - var instance = objc_msgSend_IntPtr(alloced, Selector.GetHandle("init")); - if (instance == IntPtr.Zero) return null; - - return Runtime.GetNSObject(instance); - } - private void RemoveNativeBottomToolbar() { if (_capsuleContainer is null) @@ -183,9 +161,16 @@ private partial void HideBottomToolbarOnPlatform() private static UIBarButtonItem CreateBarButtonItem(Toolbar.ToolbarButton toolbarButton) { UIImage? icon = null; - if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage)) + if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage) && uiImage is not null) { - icon = uiImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); + // Scale icon to 18pt to match Apple system toolbar sizing + var targetSize = new CGSize(18, 18); + var renderer = new UIGraphicsImageRenderer(targetSize); + var scaledImage = renderer.CreateImage(ctx => + { + uiImage.Draw(new CGRect(CGPoint.Empty, targetSize)); + }); + icon = scaledImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); } var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) =>