#nullable enable
using System;
namespace Terminal.Gui;
///
/// True color picker using HSL
///
public partial class ColorPicker : View
{
///
/// Creates a new instance of . Use
/// to change color model. Use
/// to change initial .
///
public ColorPicker ()
{
CanFocus = true;
TabStop = TabBehavior.TabStop;
Height = Dim.Auto ();
Width = Dim.Auto ();
ApplyStyleChanges ();
}
private readonly Dictionary _textFields = new ();
private readonly ColorModelStrategy _strategy = new ();
private TextField? _tfHex;
private Label? _lbHex;
private TextField? _tfName;
private Label? _lbName;
private Color _selectedColor = Color.Black;
// TODO: Add interface
private readonly IColorNameResolver _colorNameResolver = new W3CColors ();
private List _bars = new ();
///
/// Rebuild the user interface to reflect the new state of .
///
public void ApplyStyleChanges ()
{
Color oldValue = _selectedColor;
DisposeOldViews ();
var y = 0;
const int textFieldWidth = 4;
foreach (ColorBar bar in _strategy.CreateBars (Style.ColorModel))
{
bar.Y = y;
bar.Width = Dim.Fill (Style.ShowTextFields ? textFieldWidth : 0);
TextField? tfValue = null;
if (Style.ShowTextFields)
{
tfValue = new TextField
{
X = Pos.AnchorEnd (textFieldWidth),
Y = y,
Width = textFieldWidth
};
tfValue.HasFocusChanged += UpdateSingleBarValueFromTextField;
tfValue.Accepting += (s, _)=>UpdateSingleBarValueFromTextField(s);
_textFields.Add (bar, tfValue);
}
y++;
bar.ValueChanged += RebuildColorFromBar;
_bars.Add (bar);
Add (bar);
if (tfValue is { })
{
Add (tfValue);
}
}
if (Style.ShowColorName)
{
CreateNameField ();
}
CreateTextField ();
SelectedColor = oldValue;
SetNeedsLayout ();
}
///
/// Fired when color is changed.
///
public event EventHandler? ColorChanged;
///
protected override bool OnDrawingContent (Rectangle viewport)
{
Attribute normal = GetNormalColor ();
Driver?.SetAttribute (new (SelectedColor, normal.Background));
int y = _bars.Count + (Style.ShowColorName ? 1 : 0);
AddRune (13, y, (Rune)'■');
return true;
}
///
/// The color selected in the picker
///
public Color SelectedColor
{
get => _selectedColor;
set => SetSelectedColor (value, true);
}
///
/// Style settings for the color picker. After making changes ensure you call
/// .
///
public ColorPickerStyle Style { get; set; } = new ();
private void CreateNameField ()
{
_lbName = new ()
{
Text = "Name:",
X = 0,
Y = 3
};
_tfName = new ()
{
Y = 3,
X = 6,
Width = 20 // width of "LightGoldenRodYellow" - the longest w3c color name
};
Add (_lbName);
Add (_tfName);
var auto = new AppendAutocomplete (_tfName);
auto.SuggestionGenerator = new SingleWordSuggestionGenerator
{
AllSuggestions = _colorNameResolver.GetColorNames ().ToList ()
};
_tfName.Autocomplete = auto;
_tfName.HasFocusChanged += UpdateValueFromName;
_tfName.Accepting += (s, _) => UpdateValueFromName ();
}
private void CreateTextField ()
{
int y = _bars.Count;
if (Style.ShowColorName)
{
y++;
}
_lbHex = new ()
{
Text = "Hex:",
X = 0,
Y = y
};
_tfHex = new ()
{
Y = y,
X = 4,
Width = 8,
};
Add (_lbHex);
Add (_tfHex);
_tfHex.HasFocusChanged += UpdateValueFromTextField;
_tfHex.Accepting += (_,_)=> UpdateValueFromTextField();
}
private void DisposeOldViews ()
{
foreach (ColorBar bar in _bars.Cast ())
{
bar.ValueChanged -= RebuildColorFromBar;
if (_textFields.TryGetValue (bar, out TextField? tf))
{
Remove (tf);
tf.Dispose ();
}
Remove (bar);
bar.Dispose ();
}
_bars = new ();
_textFields.Clear ();
if (_lbHex != null)
{
Remove (_lbHex);
_lbHex.Dispose ();
_lbHex = null;
}
if (_tfHex != null)
{
Remove (_tfHex);
_tfHex.Dispose ();
_tfHex = null;
}
if (_lbName != null)
{
Remove (_lbName);
_lbName.Dispose ();
_lbName = null;
}
if (_tfName != null)
{
Remove (_tfName);
_tfName.Dispose ();
_tfName = null;
}
}
private void RebuildColorFromBar (object? sender, EventArgs e) { SetSelectedColor (_strategy.GetColorFromBars (_bars, Style.ColorModel), false); }
private void SetSelectedColor (Color value, bool syncBars)
{
if (_selectedColor != value)
{
Color old = _selectedColor;
_selectedColor = value;
ColorChanged?.Invoke (
this,
new (value));
}
SyncSubViewValues (syncBars);
}
private void SyncSubViewValues (bool syncBars)
{
if (syncBars)
{
_strategy.SetBarsToColor (_bars, _selectedColor, Style.ColorModel);
}
foreach (KeyValuePair kvp in _textFields)
{
kvp.Value.Text = kvp.Key.Value.ToString ();
}
var colorHex = _selectedColor.ToString ($"#{SelectedColor.R:X2}{SelectedColor.G:X2}{SelectedColor.B:X2}");
if (_tfName != null)
{
_tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string name) ? name : string.Empty;
}
if (_tfHex != null)
{
_tfHex.Text = colorHex;
}
SetNeedsLayout ();
}
private void UpdateSingleBarValueFromTextField (object? sender, HasFocusEventArgs e)
{
// if the new value of Focused is true then it is an enter event so ignore
if (e.NewValue)
{
return;
}
// it is a leave event so update
UpdateSingleBarValueFromTextField (sender);
}
private void UpdateSingleBarValueFromTextField (object? sender)
{
foreach (KeyValuePair kvp in _textFields)
{
if (kvp.Value == sender)
{
if (int.TryParse (kvp.Value.Text, out int v))
{
kvp.Key.Value = v;
}
}
}
}
private void UpdateValueFromName (object? sender, HasFocusEventArgs e)
{
// if the new value of Focused is true then it is an enter event so ignore
if (e.NewValue)
{
return;
}
// it is a leave event so update
UpdateValueFromName();
}
private void UpdateValueFromName ()
{
if (_tfName == null)
{
return;
}
if (_colorNameResolver.TryParseColor (_tfName.Text, out Color newColor))
{
SelectedColor = newColor;
}
else
{
// value is invalid, revert the value in the text field back to current state
SyncSubViewValues (false);
}
}
private void UpdateValueFromTextField (object? sender, HasFocusEventArgs e)
{
// if the new value of Focused is true then it is an enter event so ignore
if (e.NewValue)
{
return;
}
// it is a leave event so update
UpdateValueFromTextField ();
}
private void UpdateValueFromTextField ()
{
if (_tfHex == null)
{
return;
}
if (Color.TryParse (_tfHex.Text, out Color? newColor))
{
SelectedColor = newColor.Value;
}
else
{
// value is invalid, revert the value in the text field back to current state
SyncSubViewValues (false);
}
}
///
protected override void Dispose (bool disposing)
{
DisposeOldViews ();
base.Dispose (disposing);
}
}