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