Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/design/ToolbarDrawerToggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Toolbar drawer toggle contract

`Microsoft.Maui.IToolbar.DrawerToggleVisible` tells a platform backend whether the toolbar should render
the drawer (flyout / "hamburger") affordance in its navigation slot.

## Ownership

The value is **computed and owned by the cross-platform layer**, which is why the contract is read-only:

| Toolbar | How the value is computed |
| ------- | ------------------------- |
| `ShellToolbar` | `FlyoutBehavior == Flyout` and either the navigation stack has a single page or the back button is suppressed by a `BackButtonBehavior`. On Windows only `FlyoutBehavior == Flyout` is considered. |
| `NavigationPageToolbar` | The toolbar's parent is a `FlyoutPage`, `FlyoutPage.ShouldShowToolbarButton()` is `true`, and (outside Windows) no page has been pushed. |
| `Toolbar` (base) | Whatever a caller assigns to the settable `Microsoft.Maui.Controls.Toolbar.DrawerToggleVisible` property. |

`IToolbar.DrawerToggleVisible` has a default interface implementation returning `false`, so existing
`IToolbar` implementations stay source and binary compatible.

## Back button precedence

`BackButtonVisible` wins. When the back button is visible the navigation slot renders the back affordance
even if `DrawerToggleVisible` is `true`. Built-in backends encode this by checking `BackButtonVisible`
first and only falling through to the drawer icon (see `ToolbarExtensions.UpdateBackButton` on Android and
Tizen).

The framework also guarantees the ordering of the two notifications: the drawer toggle backing value is
updated *before* `BackButtonVisible` notifies, and the `DrawerToggleVisible` notification is raised
*after*. A backend that renders the shared navigation slot from either mapper therefore never observes a
"back button and drawer toggle are both visible" state.

## Change notification

Changes raise a handler update keyed on `"DrawerToggleVisible"`, exactly like any other toolbar property,
so an external backend maps it the same way it maps `Title` or `BackButtonVisible`:

```csharp
public class MyToolbarHandler : ElementHandler<IToolbar, MyPlatformToolbar>
{
public static readonly IPropertyMapper<IToolbar, MyToolbarHandler> Mapper =
new PropertyMapper<IToolbar, MyToolbarHandler>(ElementMapper)
{
[nameof(IToolbar.Title)] = MapTitle,
[nameof(IToolbar.BackButtonVisible)] = MapNavigationSlot,
[nameof(IToolbar.DrawerToggleVisible)] = MapNavigationSlot,
};

public MyToolbarHandler() : base(Mapper) { }

protected override MyPlatformToolbar CreatePlatformElement() => new();

static void MapTitle(MyToolbarHandler handler, IToolbar toolbar) =>
handler.PlatformView.Title = toolbar.Title;

static void MapNavigationSlot(MyToolbarHandler handler, IToolbar toolbar)
{
if (toolbar.BackButtonVisible)
handler.PlatformView.ShowBackButton();
else if (toolbar.DrawerToggleVisible)
handler.PlatformView.ShowDrawerToggle();
else
handler.PlatformView.ClearNavigationSlot();
}
}
```

Each window owns its own toolbar instance, so drawer toggle state and its notifications are tracked
per window with no additional work from the backend.
15 changes: 10 additions & 5 deletions src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,14 @@ void UpdateBackButton()

// Set this before BackButtonVisible triggers an update to the handler
// This way all useful information is present
if (Parent is FlyoutPage flyout && flyout.ShouldShowToolbarButton()
var drawerToggleVisible = Parent is FlyoutPage flyout && flyout.ShouldShowToolbarButton()
#if !WINDOWS // TODO NET 10 : Move this logic to ShouldShowToolbarButton
&& !anyPagesPushed.Value
#endif
)
_drawerToggleVisible = true;
else
_drawerToggleVisible = false;
;

var drawerToggleVisibleChanged = _drawerToggleVisible != drawerToggleVisible;
_drawerToggleVisible = drawerToggleVisible;

// Once we have better logic inside core to handle backbutton visiblity this
// code should all go away.
Expand Down Expand Up @@ -219,6 +219,11 @@ void UpdateBackButton()

_userChanged = false;
}

// Notified last so that BackButtonVisible (which takes precedence in the navigation slot)
// is already up to date when platform backends react to the drawer toggle change.
if (drawerToggleVisibleChanged)
NotifyPropertyChanged(nameof(DrawerToggleVisible));
}

void ApplyChanges(NavigationPage navigationPage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,5 @@ virtual Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutTemplatedConte
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeColorProperty -> Microsoft.Maui.Controls.BindableProperty
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeTextColorProperty -> Microsoft.Maui.Controls.BindableProperty
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeTextProperty -> Microsoft.Maui.Controls.BindableProperty
static Microsoft.Maui.Controls.Toolbar.MapDrawerToggleVisible(Microsoft.Maui.Handlers.IToolbarHandler! arg1, Microsoft.Maui.Controls.Toolbar! arg2) -> void
static Microsoft.Maui.Controls.Toolbar.MapDrawerToggleVisible(Microsoft.Maui.Handlers.ToolbarHandler! arg1, Microsoft.Maui.Controls.Toolbar! arg2) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,5 @@ Microsoft.Maui.Controls.Label.~Label() -> void
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeColorProperty -> Microsoft.Maui.Controls.BindableProperty
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeTextColorProperty -> Microsoft.Maui.Controls.BindableProperty
~static readonly Microsoft.Maui.Controls.TabbedPage.BadgeTextProperty -> Microsoft.Maui.Controls.BindableProperty
static Microsoft.Maui.Controls.Toolbar.MapDrawerToggleVisible(Microsoft.Maui.Handlers.IToolbarHandler! handler, Microsoft.Maui.Controls.Toolbar! toolbar) -> void
static Microsoft.Maui.Controls.Toolbar.MapDrawerToggleVisible(Microsoft.Maui.Handlers.ToolbarHandler! handler, Microsoft.Maui.Controls.Toolbar! toolbar) -> void
12 changes: 10 additions & 2 deletions src/Controls/src/Core/ShellToolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,23 @@ internal void ApplyChanges()

var flyoutBehavior = (_shell as IFlyoutView).FlyoutBehavior;
#if WINDOWS
_drawerToggleVisible = flyoutBehavior is FlyoutBehavior.Flyout;
var drawerToggleVisible = flyoutBehavior is FlyoutBehavior.Flyout;
#else
_drawerToggleVisible = flyoutBehavior is FlyoutBehavior.Flyout && (stack.Count <= 1 || !backButtonVisible);
var drawerToggleVisible = flyoutBehavior is FlyoutBehavior.Flyout && (stack.Count <= 1 || !backButtonVisible);
#endif
var drawerToggleVisibleChanged = _drawerToggleVisible != drawerToggleVisible;
_drawerToggleVisible = drawerToggleVisible;

BackButtonVisible = backButtonVisible && stack.Count > 1;
BackButtonEnabled = _backButtonBehavior?.IsEnabled ?? true;
BackButtonAccessibilityLabel = _backButtonBehavior?.AccessibilityLabel;
ToolbarItems = _toolbarTracker.ToolbarItems;

// Notified after BackButtonVisible (which takes precedence in the navigation slot) so that
// platform backends observe a fully consistent toolbar state.
if (drawerToggleVisibleChanged)
NotifyPropertyChanged(nameof(DrawerToggleVisible));

UpdateTitle();

Func<bool> getDefaultNavBarIsVisible = () =>
Expand Down
8 changes: 8 additions & 0 deletions src/Controls/src/Core/Toolbar/Toolbar.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ public static void MapBackButtonVisible(ToolbarHandler arg1, Toolbar arg2) =>
public static void MapIsVisible(ToolbarHandler arg1, Toolbar arg2) =>
MapIsVisible((IToolbarHandler)arg1, arg2);

public static void MapDrawerToggleVisible(ToolbarHandler arg1, Toolbar arg2) =>
MapDrawerToggleVisible((IToolbarHandler)arg1, arg2);



public static void MapBarTextColor(IToolbarHandler arg1, Toolbar arg2)
Expand Down Expand Up @@ -258,6 +261,11 @@ public static void MapIsVisible(IToolbarHandler arg1, Toolbar arg2)
arg1.PlatformView.UpdateIsVisible(arg2);
}

public static void MapDrawerToggleVisible(IToolbarHandler arg1, Toolbar arg2)
{
arg1.PlatformView.UpdateBackButton(arg2);
}




Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Toolbar/Toolbar.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ internal static void RemapForControls()
ToolbarHandler.Mapper.ReplaceMapping<Toolbar, IToolbarHandler>(nameof(Toolbar.BarBackground), MapBarBackground);
ToolbarHandler.Mapper.ReplaceMapping<Toolbar, IToolbarHandler>(nameof(Toolbar.BarTextColor), MapBarTextColor);
#endif
#if ANDROID || TIZEN
// Android and Tizen render the drawer (hamburger) affordance in the same navigation slot as the
// back button, so a drawer toggle change has to re-evaluate that slot.
ToolbarHandler.Mapper.ReplaceMapping<Toolbar, IToolbarHandler>(nameof(IToolbar.DrawerToggleVisible), MapDrawerToggleVisible);
#endif
#if WINDOWS
ToolbarHandler.Mapper.ReplaceMapping<Toolbar, IToolbarHandler>(nameof(Toolbar.BackButtonEnabled), MapBackButtonEnabled);
ToolbarHandler.Mapper.ReplaceMapping<Toolbar, IToolbarHandler>(PlatformConfiguration.WindowsSpecific.Page.ToolbarPlacementProperty.PropertyName, MapToolbarPlacement);
Expand Down
8 changes: 8 additions & 0 deletions src/Controls/src/Core/Toolbar/Toolbar.Tizen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public static void MapBackButtonVisible(ToolbarHandler handler, Toolbar toolbar)
public static void MapIsVisible(ToolbarHandler handler, Toolbar toolbar) =>
MapIsVisible((IToolbarHandler)handler, toolbar);

public static void MapDrawerToggleVisible(ToolbarHandler handler, Toolbar toolbar) =>
MapDrawerToggleVisible((IToolbarHandler)handler, toolbar);

public static void MapBackButtonAccessibilityLabel(ToolbarHandler handler, Toolbar toolbar) =>
MapBackButtonAccessibilityLabel((IToolbarHandler)handler, toolbar);

Expand Down Expand Up @@ -94,6 +97,11 @@ public static void MapIsVisible(IToolbarHandler handler, Toolbar toolbar)
handler.PlatformView.UpdateIsVisible(toolbar);
}

public static void MapDrawerToggleVisible(IToolbarHandler handler, Toolbar toolbar)
{
handler.PlatformView.UpdateBackButton(toolbar);
}

public static void MapBarBackground(IToolbarHandler handler, Toolbar toolbar)
{
handler.PlatformView.UpdateBarBackgroundColor(toolbar);
Expand Down
7 changes: 7 additions & 0 deletions src/Controls/src/Core/Toolbar/Toolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ private protected void SetProperty<T>(ref T backingStore, T value,
return;

backingStore = value;
NotifyPropertyChanged(propertyName);
}

// Used by derived toolbars that assign a backing field directly (to keep a specific update ordering)
// but still need the handler and PropertyChanged subscribers to observe the change afterwards.
private protected void NotifyPropertyChanged(string propertyName)
{
Handler?.UpdateValue(propertyName);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Expand Down
Loading
Loading