using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace SheetMe.Designer.Views;
///
/// Figma 식 색상 피커([200]SheetMe ColorPickerWindow 이식) — 채도/명도(SV) 사각형 + 색상(Hue) 슬라이더
/// + HEX·RGB 입력 + 프리셋/최근색. 정적 호출: 확정 시 "#RRGGBB" 반환, 취소 시 null.
/// 셸 색은 앱 테마 토큰(B.*) — SV/Hue/스와치는 색상 원본 그대로.
///
public sealed class ColorPickerWindow : Window
{
#region Member Fields
private static readonly List recent = new();
private double hue360; // H 0..360
private double sat; // S 0..1
private double val; // V 0..1
private bool syncing; // 텍스트박스 ↔ 상태 갱신 재진입 방지
private readonly Canvas svArea = new() { Width = 248, Height = 168, ClipToBounds = true, Cursor = Cursors.Cross };
private readonly Rectangle svHue = new() { Width = 248, Height = 168 };
private readonly Ellipse svThumb = new() { Width = 14, Height = 14, Stroke = Brushes.White, StrokeThickness = 2, IsHitTestVisible = false };
private readonly Canvas hueArea = new() { Width = 248, Height = 16, ClipToBounds = true, Cursor = Cursors.Cross };
private readonly Border hueThumb = new() { Width = 6, Height = 20, BorderBrush = Brushes.White, BorderThickness = new Thickness(2), CornerRadius = new CornerRadius(2), IsHitTestVisible = false };
private readonly Border preview = new() { Width = 40, Height = 40, CornerRadius = new CornerRadius(6), BorderThickness = new Thickness(1) };
private readonly TextBox hexBox = new() { Width = 92, VerticalContentAlignment = VerticalAlignment.Center };
private readonly TextBox rBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
private readonly TextBox gBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
private readonly TextBox bBox = new() { Width = 46, VerticalContentAlignment = VerticalAlignment.Center };
#endregion
#region Properties
/// 확정된 "#RRGGBB"(취소 시 null)
public string? Result { get; private set; }
#endregion
#region Methods
/// 색상 선택 대화상자 — 확정 시 "#RRGGBB", 취소 시 null. initialHex 로 초기색 지정.
public static string? Pick(Window? owner, string? initialHex)
{
var window = new ColorPickerWindow(initialHex);
if (owner is not null)
{
window.Owner = owner;
}
return window.ShowDialog() == true ? window.Result : null;
}
private ColorPickerWindow(string? initialHex)
{
Title = "색상 선택";
// 암시 Window 스타일은 파생 클래스에 적용되지 않는다 — 직접 걸지 않으면 다크에서 흰 창이 된다
if (Application.Current?.TryFindResource("ThemedWindow") is Style themed)
{
Style = themed;
}
Width = 300;
SizeToContent = SizeToContent.Height;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
ShowInTaskbar = false;
FontSize = 12.5;
var line = FindBrush("B.Line");
var muted = FindBrush("B.Muted");
preview.BorderBrush = line;
var initial = ParseHex(initialHex) ?? Color.FromRgb(0x2F, 0x6D, 0xF0);
(hue360, sat, val) = RgbToHsv(initial.R, initial.G, initial.B);
var root = new StackPanel { Margin = new Thickness(14) };
// ── SV 사각형(흰색→hue 가로 + 투명→검정 세로) ──
svArea.Children.Add(svHue);
svArea.Children.Add(new Rectangle
{
Width = 248, Height = 168,
Fill = new LinearGradientBrush(Color.FromArgb(255, 255, 255, 255), Color.FromArgb(0, 255, 255, 255), new Point(0, 0), new Point(1, 0)),
});
svArea.Children.Add(new Rectangle
{
Width = 248, Height = 168,
Fill = new LinearGradientBrush(Color.FromArgb(0, 0, 0, 0), Color.FromArgb(255, 0, 0, 0), new Point(0, 0), new Point(0, 1)),
});
svArea.Children.Add(svThumb);
svArea.MouseLeftButtonDown += (_, e) => { svArea.CaptureMouse(); UpdateSvFrom(e.GetPosition(svArea)); };
svArea.MouseMove += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed && svArea.IsMouseCaptured) UpdateSvFrom(e.GetPosition(svArea)); };
svArea.MouseLeftButtonUp += (_, _) => svArea.ReleaseMouseCapture();
root.Children.Add(new Border { Child = svArea, CornerRadius = new CornerRadius(6), ClipToBounds = true, Margin = new Thickness(0, 0, 0, 10) });
// ── Hue 슬라이더(무지개) ──
hueArea.Children.Add(new Rectangle
{
Width = 248, Height = 16,
Fill = new LinearGradientBrush(new GradientStopCollection
{
new(Color.FromRgb(255, 0, 0), 0), new(Color.FromRgb(255, 255, 0), 1 / 6.0), new(Color.FromRgb(0, 255, 0), 2 / 6.0),
new(Color.FromRgb(0, 255, 255), 3 / 6.0), new(Color.FromRgb(0, 0, 255), 4 / 6.0), new(Color.FromRgb(255, 0, 255), 5 / 6.0), new(Color.FromRgb(255, 0, 0), 1),
}, new Point(0, 0), new Point(1, 0)),
RadiusX = 8, RadiusY = 8,
});
hueArea.Children.Add(hueThumb);
Canvas.SetTop(hueThumb, -2);
hueArea.MouseLeftButtonDown += (_, e) => { hueArea.CaptureMouse(); UpdateHueFrom(e.GetPosition(hueArea)); };
hueArea.MouseMove += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed && hueArea.IsMouseCaptured) UpdateHueFrom(e.GetPosition(hueArea)); };
hueArea.MouseLeftButtonUp += (_, _) => hueArea.ReleaseMouseCapture();
root.Children.Add(new Border { Child = hueArea, Margin = new Thickness(0, 0, 0, 12) });
// ── 미리보기 + HEX / RGB ──
var inputs = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 12) };
inputs.Children.Add(preview);
var fields = new StackPanel { Margin = new Thickness(12, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
var hexRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
hexRow.Children.Add(new TextBlock { Text = "HEX", Width = 30, VerticalAlignment = VerticalAlignment.Center, Foreground = muted });
hexRow.Children.Add(hexBox);
fields.Children.Add(hexRow);
var rgbRow = new StackPanel { Orientation = Orientation.Horizontal };
rgbRow.Children.Add(new TextBlock { Text = "RGB", Width = 30, VerticalAlignment = VerticalAlignment.Center, Foreground = muted });
rgbRow.Children.Add(rBox);
rgbRow.Children.Add(new Border { Width = 4 });
rgbRow.Children.Add(gBox);
rgbRow.Children.Add(new Border { Width = 4 });
rgbRow.Children.Add(bBox);
fields.Children.Add(rgbRow);
inputs.Children.Add(fields);
root.Children.Add(inputs);
hexBox.LostKeyboardFocus += (_, _) => CommitHex();
hexBox.KeyDown += (_, e) => { if (e.Key == Key.Enter) CommitHex(); };
foreach (var box in new[] { rBox, gBox, bBox })
{
box.LostKeyboardFocus += (_, _) => CommitRgb();
box.KeyDown += (_, e) => { if (e.Key == Key.Enter) CommitRgb(); };
}
// ── 프리셋 + 최근색 ──
root.Children.Add(SwatchGrid("프리셋", new[]
{
"#000000", "#374151", "#6B727B", "#C7CDD4", "#FFFFFF", "#C0392B", "#E67E22", "#F1C40F",
"#1F9D55", "#16A085", "#2F6DF0", "#2980B9", "#8E44AD", "#EC4899", "#7F1D1D", "#FDE68A",
}));
if (recent.Count > 0)
{
root.Children.Add(SwatchGrid("최근 사용", recent.ToArray()));
}
// ── 버튼 ──
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) };
var ok = new Button { Content = "적용", MinWidth = 64, Margin = new Thickness(0, 0, 8, 0), IsDefault = true };
try { ok.Style = (Style)FindResource("Primary"); } catch { /* 테마 미로드 시 기본 */ }
ok.Click += (_, _) => { Result = CurrentHex(); AddRecent(Result); DialogResult = true; };
var cancel = new Button { Content = "취소", MinWidth = 64, IsCancel = true };
cancel.Click += (_, _) => DialogResult = false;
buttons.Children.Add(ok);
buttons.Children.Add(cancel);
root.Children.Add(buttons);
Content = root;
RefreshAll();
}
private FrameworkElement SwatchGrid(string title, string[] hexes)
{
var section = new StackPanel { Margin = new Thickness(0, 2, 0, 0) };
section.Children.Add(new TextBlock { Text = title, FontSize = 11, Foreground = FindBrush("B.Muted"), Margin = new Thickness(0, 0, 0, 4) });
var wrap = new WrapPanel();
foreach (var hex in hexes)
{
var swatch = new Button
{
Width = 22, Height = 22, Margin = new Thickness(0, 0, 4, 4), Padding = new Thickness(0),
ToolTip = hex,
Background = new SolidColorBrush(ParseHex(hex) ?? Colors.White),
};
// 견본 테두리는 테마색이 아니라 견본 자신의 명도에서 정한다 — 테마 테두리를 쓰면
// 라이트에서 흰 견본이, 다크에서 검정 견본이 배경에 묻혀 빈칸으로 보인다.
swatch.BorderBrush = ContrastEdge(ParseHex(hex) ?? Colors.White);
var captured = hex;
swatch.Click += (_, _) =>
{
if (ParseHex(captured) is not { } color)
{
return;
}
(hue360, sat, val) = RgbToHsv(color.R, color.G, color.B);
RefreshAll();
};
wrap.Children.Add(swatch);
}
section.Children.Add(wrap);
return section;
}
private static Brush FindBrush(string key)
=> Application.Current.TryFindResource(key) as Brush ?? Brushes.Gray;
///
/// 색 견본의 윤곽선 — 견본 자신의 밝기에 따라 어두운/밝은 테두리를 고른다.
/// 테마 토큰(B.Line2)을 쓰면 라이트에서 흰 견본이, 다크에서 검정 견본이 배경에 묻혀
/// 빈칸으로 보인다. 견본은 색 자체가 데이터라 배경이 아니라 견본 기준으로 판단해야 한다.
///
private static Brush ContrastEdge(Color color)
{
// WCAG 상대휘도의 단순 근사 — 견본 테두리 판정에는 이 정도면 충분하다
var luminance = (0.2126 * color.R + 0.7152 * color.G + 0.0722 * color.B) / 255.0;
var brush = new SolidColorBrush(luminance > 0.6
? Color.FromRgb(0x8A, 0x8A, 0x8A)
: Color.FromRgb(0xD0, 0xD0, 0xD0));
brush.Freeze();
return brush;
}
// ── 상호작용 → 상태 ──
private void UpdateSvFrom(Point point)
{
sat = Math.Clamp(point.X / svArea.Width, 0, 1);
val = 1 - Math.Clamp(point.Y / svArea.Height, 0, 1);
RefreshAll();
}
private void UpdateHueFrom(Point point)
{
hue360 = Math.Clamp(point.X / hueArea.Width, 0, 1) * 360;
RefreshAll();
}
private void CommitHex()
{
if (syncing)
{
return;
}
if (ParseHex(hexBox.Text) is not { } color)
{
RefreshAll();
return;
}
(hue360, sat, val) = RgbToHsv(color.R, color.G, color.B);
RefreshAll();
}
private void CommitRgb()
{
if (syncing)
{
return;
}
if (byte.TryParse(rBox.Text, out var r) && byte.TryParse(gBox.Text, out var g) && byte.TryParse(bBox.Text, out var b))
{
(hue360, sat, val) = RgbToHsv(r, g, b);
}
RefreshAll();
}
// ── 상태 → 화면 ──
private void RefreshAll()
{
var (hueR, hueG, hueB) = HsvToRgb(hue360, 1, 1);
svHue.Fill = new SolidColorBrush(Color.FromRgb(hueR, hueG, hueB));
Canvas.SetLeft(svThumb, sat * svArea.Width - 7);
Canvas.SetTop(svThumb, (1 - val) * svArea.Height - 7);
var (r, g, b) = HsvToRgb(hue360, sat, val);
svThumb.Fill = new SolidColorBrush(Color.FromRgb(r, g, b));
Canvas.SetLeft(hueThumb, hue360 / 360 * hueArea.Width - 3);
preview.Background = new SolidColorBrush(Color.FromRgb(r, g, b));
syncing = true;
hexBox.Text = CurrentHex();
rBox.Text = r.ToString();
gBox.Text = g.ToString();
bBox.Text = b.ToString();
syncing = false;
}
private string CurrentHex()
{
var (r, g, b) = HsvToRgb(hue360, sat, val);
return $"#{r:X2}{g:X2}{b:X2}";
}
private static void AddRecent(string? hex)
{
if (string.IsNullOrEmpty(hex))
{
return;
}
recent.Remove(hex);
recent.Insert(0, hex);
while (recent.Count > 16)
{
recent.RemoveAt(recent.Count - 1);
}
}
// ── 색 변환 ──
private static Color? ParseHex(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
var s = text.Trim().TrimStart('#');
if (s.Length == 3)
{
s = $"{s[0]}{s[0]}{s[1]}{s[1]}{s[2]}{s[2]}";
}
if (s.Length != 6)
{
return null;
}
return int.TryParse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
? Color.FromRgb((byte)((value >> 16) & 0xFF), (byte)((value >> 8) & 0xFF), (byte)(value & 0xFF))
: null;
}
private static (double H, double S, double V) RgbToHsv(byte r, byte g, byte b)
{
double rd = r / 255.0, gd = g / 255.0, bd = b / 255.0;
double max = Math.Max(rd, Math.Max(gd, bd)), min = Math.Min(rd, Math.Min(gd, bd));
double d = max - min, h = 0;
if (d != 0)
{
if (max == rd)
{
h = 60 * (((gd - bd) / d) % 6);
}
else if (max == gd)
{
h = 60 * (((bd - rd) / d) + 2);
}
else
{
h = 60 * (((rd - gd) / d) + 4);
}
}
if (h < 0)
{
h += 360;
}
return (h, max == 0 ? 0 : d / max, max);
}
private static (byte R, byte G, byte B) HsvToRgb(double h, double s, double v)
{
double c = v * s, x = c * (1 - Math.Abs((h / 60 % 2) - 1)), m = v - c;
double r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; }
else if (h < 120) { r = x; g = c; }
else if (h < 180) { g = c; b = x; }
else if (h < 240) { g = x; b = c; }
else if (h < 300) { r = x; b = c; }
else { r = c; b = x; }
return ((byte)Math.Round((r + m) * 255), (byte)Math.Round((g + m) * 255), (byte)Math.Round((b + m) * 255));
}
#endregion
}