using System.Globalization;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
// Avalonia's Button, not Android.Widget's. .NET for Android puts Android.Widget in this project's
// implicit usings, so the bare name is ambiguous — the same collision the namespace itself causes
// for Android.App. See the note at the top of MainActivity.
using Application = Avalonia.Application;
using Button = Avalonia.Controls.Button;
namespace DodoSSH.Client.Android.Views;
///
/// One destination in the bottom bar.
///
///
///
/// Written in C# rather than as a XAML control because it is a button with an icon, a label and a rule about
/// the indicator, and a templated control for that would be more machinery than the thing it draws.
///
///
/// ◆ The indicator is a pill behind the icon, where it was a bar along the top edge. The bar was put at
/// the top because the bottom of the screen is under the system's gesture bar, and an indicator drawn there is
/// one nobody sees; the pill answers the same worry from the other side, by sitting in the middle of the
/// control rather than at either edge. It is the shape Android's own navigation bar has drawn since Material 3
/// and the one the 2026-10-04 redesign asks for. The rule the bar existed to keep is kept: the current entry is
/// still told apart three ways — the filled pill, the accent colour and the heavier label — because colour
/// alone at this size is the one signal a user with a colour deficiency would be left with.
///
///
/// ◆ An icon from the embedded Material font rather than a Unicode glyph. The glyphs (▤ ⌗ ⚙) came from
/// whichever symbol font the phone had, at whatever weight it drew them, and ⚙ meant both this tab and the
/// Preferences row under it (audit M7). The icon font is the one the desktop's rail uses.
///
///
/// A count, when there is one. Connections carries the number of open sessions in a green badge, which
/// was otherwise only said by the strip of shells above the bar. Zero draws nothing: a "0" is a number about
/// nothing, and the badge being absent already says it.
///
///
/// It has a name. Its content is a panel, and Avalonia's automation peer reported that panel's type name
/// to TalkBack — "Avalonia.Controls.Panel", three times across the bar (audit H4). The label is the name.
///
///
/// Every colour here is resolved from Theme/Palette.axaml rather than written out. A nav bar that
/// named its own blues is exactly how the fourth place a colour appears ends up one shade off — which is the
/// argument the desktop head's Ellipse.dot style makes for being a style rather than a converter.
///
///
internal sealed class NavButton : Button
{
public static readonly StyledProperty LabelProperty =
AvaloniaProperty.Register(nameof(Label), string.Empty);
/// The icon drawn above the label: one codepoint of the embedded Material Icons font.
public static readonly StyledProperty GlyphProperty =
AvaloniaProperty.Register(nameof(Glyph), string.Empty);
public static readonly StyledProperty IsCurrentProperty =
AvaloniaProperty.Register(nameof(IsCurrent));
/// A count drawn over the icon, or zero for none.
public static readonly StyledProperty CountProperty =
AvaloniaProperty.Register(nameof(Count));
private readonly Border pill;
private readonly TextBlock glyph;
private readonly TextBlock text;
private readonly Border badge;
private readonly TextBlock badgeText;
// Resolved from the palette once the control is in the tree; see Repaint.
private IBrush? currentBrush;
private IBrush? currentIconBrush;
private IBrush? idleBrush;
private IBrush? pillBrush;
public NavButton()
{
Background = Brushes.Transparent;
BorderThickness = new Thickness(0);
CornerRadius = new CornerRadius(0);
Padding = new Thickness(0);
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
glyph = new TextBlock
{
FontSize = 21,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
AutomationProperties.SetAccessibilityView(glyph, AccessibilityView.Raw);
(badge, badgeText) = MakeBadge();
pill = new Border
{
Width = 64,
Height = 32,
CornerRadius = new CornerRadius(16),
HorizontalAlignment = HorizontalAlignment.Center,
Background = Brushes.Transparent,
Child = glyph,
};
text = new TextBlock
{
FontSize = 12,
FontWeight = FontWeight.Medium,
HorizontalAlignment = HorizontalAlignment.Center,
};
Content = new StackPanel
{
Spacing = 4,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Children = { new Panel { Children = { pill, badge } }, text },
};
}
/// The count's badge, over the pill's top-right corner.
///
/// Over the corner rather than beside the label, so it reads as belonging to the icon and costs the
/// label no width.
///
private static (Border Badge, TextBlock Text) MakeBadge()
{
var count = new TextBlock
{
FontSize = 10.5,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var holder = new Border
{
MinWidth = 18,
Height = 18,
CornerRadius = new CornerRadius(9),
Padding = new Thickness(5, 0),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -4, 8, 0),
IsVisible = false,
Child = count,
};
return (holder, count);
}
public string Label
{
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
///
public string Glyph
{
get => GetValue(GlyphProperty);
set => SetValue(GlyphProperty, value);
}
/// Whether this is the destination currently showing.
public bool IsCurrent
{
get => GetValue(IsCurrentProperty);
set => SetValue(IsCurrentProperty, value);
}
///
public int Count
{
get => GetValue(CountProperty);
set => SetValue(CountProperty, value);
}
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == LabelProperty)
{
text.Text = Label;
AutomationProperties.SetName(this, Label);
}
else if (change.Property == GlyphProperty)
{
glyph.Text = Glyph;
}
else if (change.Property == IsCurrentProperty)
{
Repaint();
}
else if (change.Property == CountProperty)
{
badge.IsVisible = Count > 0;
badgeText.Text = Count.ToString(CultureInfo.InvariantCulture);
}
}
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// Resolved here rather than in the constructor: application resources are not reachable while the
// control is still being constructed, and a hard-coded stack would drift from Palette.axaml.
if (Application.Current?.FindResource("IconFont") is FontFamily icons)
{
glyph.FontFamily = icons;
}
currentBrush = Application.Current?.FindResource("Text") as IBrush;
currentIconBrush = Application.Current?.FindResource("AccentText") as IBrush;
idleBrush = Application.Current?.FindResource("TextFaint") as IBrush;
pillBrush = Application.Current?.FindResource("AccentSoft") as IBrush;
badge.Background = Application.Current?.FindResource("Live") as IBrush;
badgeText.Foreground = Application.Current?.FindResource("Canvas") as IBrush;
// IsCurrent is normally bound and so has usually already been set by the time the template applies,
// which is why this runs again here rather than only on the property change.
Repaint();
}
private void Repaint()
{
// The pill, the colour and the weight together. See the remark on the type.
var paint = IsCurrent ? currentBrush : idleBrush;
var iconPaint = IsCurrent ? currentIconBrush : idleBrush;
if (paint is not null)
{
text.Foreground = paint;
}
if (iconPaint is not null)
{
glyph.Foreground = iconPaint;
}
text.FontWeight = IsCurrent ? FontWeight.Bold : FontWeight.Medium;
pill.Background = IsCurrent && pillBrush is not null ? pillBrush : Brushes.Transparent;
}
}