using Avalonia; 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 a glyph, a label and a rule about /// the accent bar, and a templated control for that would be more machinery than the thing it draws. /// /// /// The accent bar is on top, not underneath. The v2 design distinguishes the current destination with /// colour and weight alone and draws no bar at all; the bar is kept, and kept at the top, for a reason the /// design does not have to think about. The bottom of a phone screen is under the system's own gesture bar, /// so an indicator drawn there is one the user cannot see — and colour alone at 9pt on a dark background is /// the single signal a user with a colour deficiency would be left with. Weight, colour and the bar are /// three, which is the right number for the control that says where you are. /// /// /// 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 glyph drawn above the label. /// /// A character rather than an icon asset. The four the design uses — ▤ ⌗ ⚿ ≣ — are all in the fonts /// Android already has, so shipping four vectors to draw them would buy nothing but four more files to /// keep in step with the palette. /// public static readonly StyledProperty GlyphProperty = AvaloniaProperty.Register(nameof(Glyph), string.Empty); public static readonly StyledProperty IsCurrentProperty = AvaloniaProperty.Register(nameof(IsCurrent)); private readonly Border indicator; private readonly TextBlock glyph; private readonly TextBlock text; // Resolved from the palette once the control is in the tree; see Repaint. private IBrush? currentBrush; private IBrush? idleBrush; 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; indicator = new Border { Height = 2, VerticalAlignment = VerticalAlignment.Top, Background = Brushes.Transparent, }; glyph = new TextBlock { FontSize = 15, HorizontalAlignment = HorizontalAlignment.Center, }; text = new TextBlock { FontSize = 9, FontWeight = FontWeight.Medium, HorizontalAlignment = HorizontalAlignment.Center, }; var stack = new StackPanel { Spacing = 3, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Children = { glyph, text }, }; Content = new Panel { Children = { indicator, stack } }; } 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); } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == LabelProperty) { text.Text = Label; } else if (change.Property == GlyphProperty) { glyph.Text = Glyph; } else if (change.Property == IsCurrentProperty) { Repaint(); } else if (change.Property == FontFamilyProperty) { text.FontFamily = FontFamily; } } /// 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("MonoFont") is FontFamily mono) { text.FontFamily = mono; } currentBrush = Application.Current?.FindResource("AccentText") as IBrush; idleBrush = Application.Current?.FindResource("TextFaint") 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() { // Both the label and the bar, not just the bar — and the weight as well. See the remark on the type. var paint = IsCurrent ? currentBrush : idleBrush; if (paint is not null) { glyph.Foreground = paint; text.Foreground = paint; } text.FontWeight = IsCurrent ? FontWeight.SemiBold : FontWeight.Medium; indicator.Background = IsCurrent && currentBrush is not null ? currentBrush : Brushes.Transparent; } }