Files
CapySR/gui/Ui.cs
T
2026-10-17 20:42:15 -07:00

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 });
}
}
}