430 lines
21 KiB
C#
430 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
|
|
namespace CapyLauncher
|
|
{
|
|
public class PickItem : System.ComponentModel.INotifyPropertyChanged
|
|
{
|
|
ImageSource icon;
|
|
bool requested;
|
|
|
|
public int Id { get; set; }
|
|
public string Title { get; set; }
|
|
public string Sub { get; set; }
|
|
public Brush Accent { get; set; }
|
|
public object Tag { get; set; }
|
|
public string IconPath { get; set; }
|
|
public Visibility DotVisibility => icon == null ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
public ImageSource Icon
|
|
{
|
|
get
|
|
{
|
|
if (!requested && IconPath != null)
|
|
{
|
|
requested = true;
|
|
Load();
|
|
}
|
|
return icon;
|
|
}
|
|
}
|
|
|
|
async void Load()
|
|
{
|
|
icon = await Icons.Get(IconPath);
|
|
if (icon == null) return;
|
|
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Icon)));
|
|
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(DotVisibility)));
|
|
}
|
|
|
|
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
|
|
}
|
|
|
|
public class Picker : Border
|
|
{
|
|
readonly TextBlock title = new TextBlock { TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center };
|
|
readonly Image chosen = new Image { Width = 22, Height = 22, Margin = new Thickness(0, 0, 9, 0), VerticalAlignment = VerticalAlignment.Center, Visibility = Visibility.Collapsed };
|
|
readonly TextBlock sub = new TextBlock { FontSize = 11.5, Margin = new Thickness(8, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
|
|
readonly Popup popup = new Popup { AllowsTransparency = true, StaysOpen = false, Placement = PlacementMode.Bottom, PopupAnimation = PopupAnimation.Fade };
|
|
readonly TextBox search = new TextBox { Tag = "Type to search", Margin = new Thickness(0, 0, 0, 6), FontSize = 12.5 };
|
|
readonly ListBox list = new ListBox { Background = Brushes.Transparent, BorderThickness = new Thickness(0), MaxHeight = 320 };
|
|
readonly string placeholder;
|
|
|
|
public IList<PickItem> Items { get; set; } = new List<PickItem>();
|
|
public event Action<PickItem> Picked;
|
|
|
|
public Picker(string placeholder)
|
|
{
|
|
this.placeholder = placeholder;
|
|
Background = Ui.Brush("Field");
|
|
BorderBrush = Ui.Brush("Line");
|
|
BorderThickness = new Thickness(1);
|
|
CornerRadius = new CornerRadius(9);
|
|
Padding = new Thickness(11, 7, 10, 7);
|
|
Cursor = Cursors.Hand;
|
|
MinHeight = 34;
|
|
sub.Foreground = Ui.Brush("Faint");
|
|
|
|
var row = new DockPanel();
|
|
var chevron = new TextBlock { Text = "", FontFamily = (FontFamily)Application.Current.FindResource("Icons"), FontSize = 10, Foreground = Ui.Brush("Dim"), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0) };
|
|
DockPanel.SetDock(chevron, Dock.Right);
|
|
row.Children.Add(chevron);
|
|
var text = new StackPanel { Orientation = Orientation.Horizontal };
|
|
RenderOptions.SetBitmapScalingMode(chosen, BitmapScalingMode.HighQuality);
|
|
text.Children.Add(chosen);
|
|
text.Children.Add(title);
|
|
text.Children.Add(sub);
|
|
row.Children.Add(text);
|
|
Child = row;
|
|
Show(null);
|
|
|
|
list.ItemContainerStyle = (Style)Application.Current.FindResource("PickItem");
|
|
VirtualizingPanel.SetIsVirtualizing(list, true);
|
|
ScrollViewer.SetHorizontalScrollBarVisibility(list, ScrollBarVisibility.Disabled);
|
|
list.ItemTemplate = Ui.PickTemplate();
|
|
list.PreviewMouseLeftButtonUp += (s, e) => Choose();
|
|
list.KeyDown += (s, e) => { if (e.Key == Key.Enter) Choose(); };
|
|
search.TextChanged += (s, e) => Filter();
|
|
search.PreviewKeyDown += (s, e) =>
|
|
{
|
|
if (e.Key == Key.Down && list.Items.Count > 0)
|
|
{
|
|
list.SelectedIndex = 0;
|
|
((UIElement)list.ItemContainerGenerator.ContainerFromIndex(0))?.Focus();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Enter && list.Items.Count > 0)
|
|
{
|
|
list.SelectedIndex = 0;
|
|
Choose();
|
|
}
|
|
};
|
|
|
|
var panel = new DockPanel();
|
|
DockPanel.SetDock(search, Dock.Top);
|
|
panel.Children.Add(search);
|
|
panel.Children.Add(list);
|
|
popup.Child = new Border
|
|
{
|
|
Background = Ui.Brush("Bg"), BorderBrush = Ui.Brush("Line"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12),
|
|
Padding = new Thickness(8), Margin = new Thickness(0, 6, 12, 12), Child = panel,
|
|
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 24, ShadowDepth = 6, Opacity = 0.5, Color = Colors.Black },
|
|
};
|
|
popup.PlacementTarget = this;
|
|
popup.Opened += (s, e) => { search.Focus(); search.SelectAll(); };
|
|
|
|
MouseEnter += (s, e) => BorderBrush = Ui.Brush("Pink");
|
|
MouseLeave += (s, e) => { if (!popup.IsOpen) BorderBrush = Ui.Brush("Line"); };
|
|
popup.Closed += (s, e) => BorderBrush = Ui.Brush("Line");
|
|
MouseLeftButtonUp += (s, e) => Open();
|
|
}
|
|
|
|
public void Show(PickItem item)
|
|
{
|
|
chosen.Visibility = Visibility.Collapsed;
|
|
chosen.Source = null;
|
|
if (item?.IconPath != null)
|
|
{
|
|
chosen.Visibility = Visibility.Visible;
|
|
chosen.Opacity = 0;
|
|
Icons.Fill(chosen, item.IconPath);
|
|
}
|
|
title.Text = item?.Title ?? placeholder;
|
|
title.Foreground = Ui.Brush(item == null ? "Faint" : "Text");
|
|
sub.Text = item?.Sub ?? "";
|
|
}
|
|
|
|
void Open()
|
|
{
|
|
popup.MinWidth = Math.Max(ActualWidth, 280);
|
|
search.Text = "";
|
|
Filter();
|
|
popup.IsOpen = true;
|
|
}
|
|
|
|
void Filter()
|
|
{
|
|
string query = search.Text.Trim();
|
|
IEnumerable<PickItem> found = Items;
|
|
if (query.Length > 0)
|
|
{
|
|
found = Items.Where(i => i.Title.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| i.Id.ToString().StartsWith(query)
|
|
|| (i.Sub ?? "").IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0);
|
|
}
|
|
list.ItemsSource = found.Take(250).ToList();
|
|
}
|
|
|
|
void Choose()
|
|
{
|
|
if (!(list.SelectedItem is PickItem item))
|
|
return;
|
|
popup.IsOpen = false;
|
|
Show(item);
|
|
Picked?.Invoke(item);
|
|
}
|
|
}
|
|
|
|
public static class Ui
|
|
{
|
|
public static Brush Brush(string key) => (Brush)Application.Current.FindResource(key);
|
|
|
|
public static DataTemplate PickTemplate()
|
|
{
|
|
var template = new DataTemplate(typeof(PickItem));
|
|
var row = new FrameworkElementFactory(typeof(DockPanel));
|
|
var lead = new FrameworkElementFactory(typeof(Grid));
|
|
lead.SetValue(FrameworkElement.WidthProperty, 26.0);
|
|
lead.SetValue(FrameworkElement.HeightProperty, 26.0);
|
|
lead.SetValue(FrameworkElement.MarginProperty, new Thickness(0, 0, 10, 0));
|
|
var dot = new FrameworkElementFactory(typeof(Border));
|
|
dot.SetValue(Border.WidthProperty, 8.0);
|
|
dot.SetValue(Border.HeightProperty, 8.0);
|
|
dot.SetValue(Border.CornerRadiusProperty, new CornerRadius(4));
|
|
dot.SetBinding(Border.BackgroundProperty, new System.Windows.Data.Binding("Accent"));
|
|
dot.SetBinding(UIElement.VisibilityProperty, new System.Windows.Data.Binding("DotVisibility"));
|
|
lead.AppendChild(dot);
|
|
var picture = new FrameworkElementFactory(typeof(Image));
|
|
picture.SetBinding(Image.SourceProperty, new System.Windows.Data.Binding("Icon"));
|
|
picture.SetValue(RenderOptions.BitmapScalingModeProperty, BitmapScalingMode.HighQuality);
|
|
lead.AppendChild(picture);
|
|
row.AppendChild(lead);
|
|
var sub = new FrameworkElementFactory(typeof(TextBlock));
|
|
sub.SetValue(DockPanel.DockProperty, Dock.Right);
|
|
sub.SetBinding(TextBlock.TextProperty, new System.Windows.Data.Binding("Sub"));
|
|
sub.SetValue(TextBlock.ForegroundProperty, Brush("Faint"));
|
|
sub.SetValue(TextBlock.FontSizeProperty, 11.5);
|
|
sub.SetValue(FrameworkElement.MarginProperty, new Thickness(12, 0, 0, 0));
|
|
sub.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
|
|
row.AppendChild(sub);
|
|
var title = new FrameworkElementFactory(typeof(TextBlock));
|
|
title.SetBinding(TextBlock.TextProperty, new System.Windows.Data.Binding("Title"));
|
|
title.SetValue(TextBlock.ForegroundProperty, Brush("Text"));
|
|
title.SetValue(TextBlock.TextTrimmingProperty, TextTrimming.CharacterEllipsis);
|
|
row.AppendChild(title);
|
|
template.VisualTree = row;
|
|
return template;
|
|
}
|
|
|
|
public static TextBlock Caption(string text, double top = 0) =>
|
|
new TextBlock { Text = text, Style = (Style)Application.Current.FindResource("Caption"), Margin = new Thickness(0, top, 0, 8) };
|
|
|
|
public static TextBlock Text(string text, string color = "Text", double size = 13.5) =>
|
|
new TextBlock { Text = text, Foreground = Brush(color), FontSize = size, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis };
|
|
|
|
public static Border Section(string caption, UIElement content, string glyph = null)
|
|
{
|
|
var stack = new StackPanel();
|
|
var head = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 12) };
|
|
if (glyph != null)
|
|
head.Children.Add(new TextBlock { Text = glyph, FontFamily = (FontFamily)Application.Current.FindResource("Icons"), Foreground = Brush("Pink"), FontSize = 13, Margin = new Thickness(0, 0, 9, 0), VerticalAlignment = VerticalAlignment.Center });
|
|
head.Children.Add(new TextBlock { Text = caption, FontSize = 14, FontWeight = FontWeights.SemiBold, VerticalAlignment = VerticalAlignment.Center });
|
|
stack.Children.Add(head);
|
|
stack.Children.Add(content);
|
|
return new Border
|
|
{
|
|
Background = (Brush)new BrushConverter().ConvertFromString("#0AFFFFFF"), BorderBrush = Brush("SoftLine"), BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(14), Padding = new Thickness(18, 16, 18, 18), Margin = new Thickness(0, 0, 0, 14), Child = stack,
|
|
};
|
|
}
|
|
|
|
public static Button Button(string text, RoutedEventHandler click, string glyph = null, bool primary = false, double size = 12.5)
|
|
{
|
|
object content = text;
|
|
if (glyph != null)
|
|
{
|
|
var row = new StackPanel { Orientation = Orientation.Horizontal };
|
|
row.Children.Add(new TextBlock { Text = glyph, FontFamily = (FontFamily)Application.Current.FindResource("Icons"), FontSize = size, Margin = new Thickness(0, 1, text.Length > 0 ? 7 : 0, 0), VerticalAlignment = VerticalAlignment.Center });
|
|
if (text.Length > 0)
|
|
row.Children.Add(new TextBlock { Text = text, VerticalAlignment = VerticalAlignment.Center });
|
|
content = row;
|
|
}
|
|
var button = new Button { Content = content, FontSize = size, Padding = new Thickness(11, 6, 11, 6) };
|
|
if (primary)
|
|
{
|
|
button.Style = (Style)Application.Current.FindResource("Primary");
|
|
button.Padding = new Thickness(16, 9, 16, 9);
|
|
button.FontSize = 13.5;
|
|
}
|
|
button.Click += click;
|
|
return button;
|
|
}
|
|
|
|
public static FrameworkElement Slider(int min, int max, int value, Action<int> changed, Func<int, string> label = null, double width = double.NaN)
|
|
{
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(58) });
|
|
var slider = new Slider { Minimum = min, Maximum = max, Value = Math.Max(min, Math.Min(max, value)), VerticalAlignment = VerticalAlignment.Center, Width = width };
|
|
var shown = new TextBlock { FontFamily = (FontFamily)Application.Current.FindResource("Mono"), FontSize = 12.5, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center };
|
|
shown.Text = label != null ? label(value) : value.ToString();
|
|
Grid.SetColumn(shown, 1);
|
|
slider.ValueChanged += (s, e) =>
|
|
{
|
|
int v = (int)Math.Round(e.NewValue);
|
|
shown.Text = label != null ? label(v) : v.ToString();
|
|
changed(v);
|
|
};
|
|
grid.Children.Add(slider);
|
|
grid.Children.Add(shown);
|
|
return grid;
|
|
}
|
|
|
|
public static FrameworkElement Segments(IEnumerable<string> labels, int selected, Action<int> changed)
|
|
{
|
|
var row = new WrapPanel();
|
|
string group = Guid.NewGuid().ToString();
|
|
int index = 0;
|
|
foreach (string label in labels)
|
|
{
|
|
int mine = index++;
|
|
var option = new RadioButton { Content = label, GroupName = group, Style = (Style)Application.Current.FindResource("Seg"), IsChecked = mine == selected, Margin = new Thickness(0, 0, 5, 5) };
|
|
option.Checked += (s, e) => changed(mine);
|
|
row.Children.Add(option);
|
|
}
|
|
return row;
|
|
}
|
|
|
|
public static FrameworkElement Stepper(int min, int max, int value, Action<int> changed, double width = 96)
|
|
{
|
|
var box = new Border { Background = Brush("Field"), BorderBrush = Brush("Line"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(9), Width = width, Height = 32 };
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
|
var number = new TextBox
|
|
{
|
|
Style = null, Text = value.ToString(), Background = Brushes.Transparent, BorderThickness = new Thickness(0),
|
|
HorizontalContentAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
|
FontFamily = (FontFamily)Application.Current.FindResource("Mono"), FontSize = 12.5, Foreground = Brush("Text"),
|
|
CaretBrush = Brush("Pink"), SelectionBrush = Brush("Pink"),
|
|
};
|
|
int current = value;
|
|
void Set(int v)
|
|
{
|
|
v = Math.Max(min, Math.Min(max, v));
|
|
number.Text = v.ToString();
|
|
if (v == current) return;
|
|
current = v;
|
|
changed(v);
|
|
}
|
|
number.LostKeyboardFocus += (s, e) => Set(int.TryParse(number.Text, out int typed) ? typed : current);
|
|
number.KeyDown += (s, e) => { if (e.Key == Key.Enter) Set(int.TryParse(number.Text, out int typed) ? typed : current); };
|
|
number.PreviewMouseWheel += (s, e) => { Set(current + (e.Delta > 0 ? 1 : -1)); e.Handled = true; };
|
|
var minus = Glyph("", () => Set(current - 1));
|
|
var plus = Glyph("", () => Set(current + 1));
|
|
Grid.SetColumn(number, 1);
|
|
Grid.SetColumn(plus, 2);
|
|
grid.Children.Add(minus);
|
|
grid.Children.Add(number);
|
|
grid.Children.Add(plus);
|
|
box.Child = grid;
|
|
return box;
|
|
}
|
|
|
|
static FrameworkElement Glyph(string glyph, Action click)
|
|
{
|
|
var text = new TextBlock { Text = glyph, FontFamily = (FontFamily)Application.Current.FindResource("Icons"), FontSize = 10, Foreground = Brush("Dim"), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
|
|
var hit = new Border { Background = Brushes.Transparent, Cursor = Cursors.Hand, Child = text };
|
|
hit.MouseEnter += (s, e) => text.Foreground = Brush("Pink");
|
|
hit.MouseLeave += (s, e) => text.Foreground = Brush("Dim");
|
|
hit.MouseLeftButtonUp += (s, e) => click();
|
|
return hit;
|
|
}
|
|
|
|
public static CheckBox Toggle(string text, bool on, Action<bool> changed)
|
|
{
|
|
var toggle = new CheckBox { Content = text, IsChecked = on, Foreground = Brush("Text"), FontSize = 12.5 };
|
|
toggle.Checked += (s, e) => changed(true);
|
|
toggle.Unchecked += (s, e) => changed(false);
|
|
return toggle;
|
|
}
|
|
|
|
public static Grid Row(params (UIElement element, GridLength width)[] cells)
|
|
{
|
|
var grid = new Grid { Margin = new Thickness(0, 0, 0, 10) };
|
|
int column = 0;
|
|
foreach (var (element, width) in cells)
|
|
{
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = width });
|
|
if (element != null)
|
|
{
|
|
Grid.SetColumn(element, column);
|
|
grid.Children.Add(element);
|
|
}
|
|
column++;
|
|
}
|
|
return grid;
|
|
}
|
|
|
|
public static GridLength Px(double px) => new GridLength(px);
|
|
public static GridLength Star(double share = 1) => new GridLength(share, GridUnitType.Star);
|
|
public static readonly GridLength Auto = GridLength.Auto;
|
|
|
|
public static FrameworkElement Portrait(AvatarInfo avatar, double size)
|
|
{
|
|
var frame = new Border { Width = size, Height = size, CornerRadius = new CornerRadius(size * 0.32), Background = Palette.Element(avatar?.Element), ClipToBounds = true };
|
|
var inner = new Grid();
|
|
inner.Children.Add(Badge(avatar, size));
|
|
var image = new Image { Width = size, Height = size, Stretch = Stretch.UniformToFill, Opacity = 0 };
|
|
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
|
|
image.Clip = new RectangleGeometry(new Rect(0, 0, size, size), size * 0.32, size * 0.32);
|
|
inner.Children.Add(image);
|
|
frame.Child = inner;
|
|
if (avatar != null)
|
|
Icons.Fill(image, Icons.Character(avatar.Id), inner.Children[0]);
|
|
return frame;
|
|
}
|
|
|
|
public static Border Badge(AvatarInfo avatar, double size)
|
|
{
|
|
var badge = new Border
|
|
{
|
|
Width = size, Height = size, CornerRadius = new CornerRadius(size * 0.32),
|
|
Background = Palette.Element(avatar?.Element),
|
|
Child = new TextBlock
|
|
{
|
|
Text = avatar?.Initials ?? "?", Foreground = (Brush)new BrushConverter().ConvertFromString("#17131F"), FontWeight = FontWeights.Bold,
|
|
FontSize = size * 0.36, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
|
},
|
|
};
|
|
return badge;
|
|
}
|
|
|
|
public static Border Chip(string text, Brush accent, string icon = null)
|
|
{
|
|
var row = new StackPanel { Orientation = Orientation.Horizontal };
|
|
if (icon != null)
|
|
{
|
|
var dotFallback = new Border { Width = 7, Height = 7, CornerRadius = new CornerRadius(3.5), Background = accent };
|
|
var view = Icons.View(icon, 16, dotFallback);
|
|
view.Margin = new Thickness(-2, 0, 5, 0);
|
|
view.VerticalAlignment = VerticalAlignment.Center;
|
|
row.Children.Add(view);
|
|
}
|
|
else
|
|
{
|
|
row.Children.Add(new Border { Width = 7, Height = 7, CornerRadius = new CornerRadius(3.5), Background = accent, Margin = new Thickness(0, 0, 6, 0), VerticalAlignment = VerticalAlignment.Center });
|
|
}
|
|
row.Children.Add(new TextBlock { Text = text, FontSize = 11.5, Foreground = Brush("Text") });
|
|
return new Border { Background = (Brush)new BrushConverter().ConvertFromString("#14FFFFFF"), CornerRadius = new CornerRadius(10), Padding = new Thickness(9, 3, 10, 3), Margin = new Thickness(0, 0, 6, 0), Child = row };
|
|
}
|
|
|
|
public static void FadeIn(UIElement element, double fromY = 10)
|
|
{
|
|
var move = new TranslateTransform(0, fromY);
|
|
element.RenderTransform = move;
|
|
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
|
element.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease });
|
|
move.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(fromY, 0, TimeSpan.FromMilliseconds(320)) { EasingFunction = ease });
|
|
}
|
|
}
|
|
}
|