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, (_, _) =>