Fixes #2800 - Color picker (supporting hsl, hsv and rgb) (#3604)

* Readonly HSL view

* Make it possible to move between bars by moving to subview

* Basically working and with mouse support

* Fix HSL to work properly with double values instead of color matching

* Fix Value on ColorPicker to match HSL values

* Fix color spectrum

* Add Swatch and better sync with text box

* Work on jitter

* ColorPicker HSL working

* More keybindings

* Add ColorModel

* Support both HSL and HSV

* Add RGB

* Better mouse handling

* WIP: AttributeView and integrate into LineDrawing
(does not currently work properly)

* Fix color picking

* Add concept of an ITool

* Add ColorPickerStyle

* Fix selected cell rendering

* Add first test for ColorPicker2

* Add more RGB tests

* Improve ColorPicker2 setup process

* Tests and fixes for keyboard changing value R

* Fix margin on bars when no textfields

* Add mouse test

* Add tests for with text field

* Add more tests and fix bug sync component text field change with hex text field

(WIP - failing tests)

* Fix tests and fix clicking in a bar label area possibly not selecting

* Move AttributeView to LineDrawing and adjust to have a 'transparent pattern' too

* Render triangle in dark gray if background is black

* Add ColorChanged event

* Resharper Cleanup

* Xml comments and public/private adjustments

* Explore replacing diagram test with fragile Subview diving

* Migrate ColorPicker_DefaultBoot to sub asserts

* Port other tests

* Replace ColorPicker with new view

* Fix ColorPicker size to match scenarios size assumptions

* Split to separate files and ignore invalid test for ColorPicker

* Ignore also in mouse version of AllViews_Enter_Leave_Events

* Remove bool _updating from ColorPicker

Now instead we are more selective about what we update when and do so deterministically

* Typo fix

* Fix ReSharper bad renames in comments for "Value"

* Refactor to single implementation of 'prompt for color' logic

- Now called PromptForColor
- Shared by LineDrawing and ProgressBarStyles scenarios

* Sum runes instead of Length

* Hide ColorBar and SetValueWithoutRaisingEvent from public API

* Move ColorEventArgs to Drawing folder

* Move ColorModel to Drawing folder

* First try at Dim.Auto for ColorPicker

* Remove explicit width/height setting in most scenarios

* Remove explicit heights

* Fixed build/test issues.
Illustrated test best practice.

* WIP: Start working on test changes and add new options to ColorPickers scenario (Color Model and show textfields).

* Fix for R indicator arrow sometimes 'falling off' the drawn area.

* Add nullable enable

* Test fixes and refactor for avoiding Begin

* Make ColorEventArgs inherit from EventArgs<Color>

* Fix Dispose not being called on bars when switching color models

* Remove 'oldColor' from test now it is not supported

* Add initial stab at ColorPickerStyle.ShowName

* Use AppendAutocomplete for color names

* Implemented resoruce based colorname resolver

* Update GetTextField to support getting the color names field
Change style setting to ShowColorName

* Color name updates when navigating away from the named color

* Restore old color picker as ColorPicker16

* Add test that shows 'Save as' is currently considered a named color ><

* Fix GetW3CColorNames

* Removed dupe colors

* Revert to old color pickers

* Nullability question marks for everyone!

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Thomas Nind
2024-08-23 00:53:04 +01:00
committed by GitHub
parent e95f821f2b
commit 38f84f7424
30 changed files with 4507 additions and 615 deletions

View File

@@ -1,6 +1,7 @@
#nullable enable
using System.Collections.Frozen;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -331,4 +332,4 @@ public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanPar
public const ColorName White = ColorName.White;
#endregion
}
}

View File

@@ -0,0 +1,11 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>Event arguments for the <see cref="Color"/> events.</summary>
public class ColorEventArgs : EventArgs<Color>
{
/// <summary>Initializes a new instance of <see cref="ColorEventArgs"/>
/// <paramref name="newColor"/>The value that is being changed to.</summary>
public ColorEventArgs (Color newColor) :base(newColor) { }
}

View File

@@ -0,0 +1,25 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>
/// Describes away of modelling color e.g. Hue
/// Saturation Lightness.
/// </summary>
public enum ColorModel
{
/// <summary>
/// Color modelled by storing Red, Green and Blue as (0-255) ints
/// </summary>
RGB,
/// <summary>
/// Color modelled by storing Hue (360 degrees), Saturation (100%) and Value (100%)
/// </summary>
HSV,
/// <summary>
/// Color modelled by storing Hue (360 degrees), Saturation (100%) and Lightness (100%)
/// </summary>
HSL
}

View File

@@ -0,0 +1,78 @@
#nullable enable
using System.Collections;
using System.Globalization;
using System.Resources;
using Terminal.Gui.Resources;
namespace Terminal.Gui;
/// <summary>
/// Provides a mapping between <see cref="Color"/> and the W3C standard color name strings.
/// </summary>
public static class ColorStrings
{
private static readonly ResourceManager _resourceManager = new (typeof (Strings));
/// <summary>
/// Gets the W3C standard string for <paramref name="color"/>.
/// </summary>
/// <param name="color">The color.</param>
/// <returns><see langword="null"/> if there is no standard color name for the specified color.</returns>
public static string? GetW3CColorName (Color color)
{
// Fetch the color name from the resource file
return _resourceManager.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentCulture);
}
/// <summary>
/// Returns the list of W3C standard color names.
/// </summary>
/// <returns></returns>
public static IEnumerable<string> GetW3CColorNames ()
{
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!)
{
string keyName = entry.Key.ToString () ?? string.Empty;
if (entry.Value is string colorName && keyName.StartsWith ('#'))
{
yield return colorName;
}
}
}
/// <summary>
/// Parses <paramref name="name"/> and returns <paramref name="color"/> if name is a W3C standard named color.
/// </summary>
/// <param name="name">The name to parse.</param>
/// <param name="color">If successful, the color.</param>
/// <returns><see langword="true"/> if <paramref name="name"/> was parsed successfully.</returns>
public static bool TryParseW3CColorName (string name, out Color color)
{
// Iterate through all resource entries to find the matching color name
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!)
{
if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase))
{
// Parse the key to extract the color components
string key = entry.Key.ToString () ?? string.Empty;
if (key.StartsWith ("#") && key.Length == 7)
{
if (int.TryParse (key.Substring (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r)
&& int.TryParse (key.Substring (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g)
&& int.TryParse (key.Substring (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b))
{
color = new (r, g, b);
return true;
}
}
}
}
color = default (Color);
return false;
}
}

View File

@@ -0,0 +1,34 @@
namespace Terminal.Gui;
/// <summary>
/// When implemented by a class, allows mapping <see cref="Color"/> to
/// human understandable name (e.g. w3c color names) and vice versa.
/// </summary>
public interface IColorNameResolver
{
/// <summary>
/// Returns the names of all known colors.
/// </summary>
/// <returns></returns>
IEnumerable<string> GetColorNames ();
/// <summary>
/// Returns <see langword="true"/> if <paramref name="color"/> is a recognized
/// color. In which case <paramref name="name"/> will be the name of the color and
/// return value will be true otherwise false.
/// </summary>
/// <param name="color"></param>
/// <param name="name"></param>
/// <returns></returns>
bool TryNameColor (Color color, out string name);
/// <summary>
/// Returns <see langword="true"/> if <paramref name="name"/> is a recognized
/// color. In which case <paramref name="color"/> will be the color the name corresponds
/// to otherwise returns false.
/// </summary>
/// <param name="name"></param>
/// <param name="color"></param>
/// <returns></returns>
bool TryParseColor (string name, out Color color);
}

View File

@@ -0,0 +1,24 @@
namespace Terminal.Gui;
/// <summary>
/// Helper class that resolves w3c color names to their hex values
/// Based on https://www.w3schools.com/colors/color_tryit.asp
/// </summary>
public class W3CColors : IColorNameResolver
{
/// <inheritdoc/>
public IEnumerable<string> GetColorNames () { return ColorStrings.GetW3CColorNames (); }
/// <inheritdoc/>
public bool TryParseColor (string name, out Color color) { return ColorStrings.TryParseW3CColorName (name, out color); }
/// <inheritdoc/>
public bool TryNameColor (Color color, out string name)
{
string answer = ColorStrings.GetW3CColorName (color);
name = answer ?? string.Empty;
return answer != null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
<!--
Microsoft ResX Schema
Version 2.0
@@ -59,225 +59,642 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ctxSelectAll" xml:space="preserve">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ctxSelectAll" xml:space="preserve">
<value>_Select All</value>
</data>
<data name="ctxDeleteAll" xml:space="preserve">
<data name="ctxDeleteAll" xml:space="preserve">
<value>_Delete All</value>
</data>
<data name="ctxCopy" xml:space="preserve">
<data name="ctxCopy" xml:space="preserve">
<value>_Copy</value>
</data>
<data name="ctxCut" xml:space="preserve">
<data name="ctxCut" xml:space="preserve">
<value>Cu_t</value>
</data>
<data name="ctxPaste" xml:space="preserve">
<data name="ctxPaste" xml:space="preserve">
<value>_Paste</value>
</data>
<data name="ctxUndo" xml:space="preserve">
<data name="ctxUndo" xml:space="preserve">
<value>_Undo</value>
</data>
<data name="ctxRedo" xml:space="preserve">
<data name="ctxRedo" xml:space="preserve">
<value>_Redo</value>
</data>
<data name="fdDirectory" xml:space="preserve">
<data name="fdDirectory" xml:space="preserve">
<value>Directory</value>
</data>
<data name="fdFile" xml:space="preserve">
<data name="fdFile" xml:space="preserve">
<value>File</value>
</data>
<data name="fdSave" xml:space="preserve">
<data name="fdSave" xml:space="preserve">
<value>Save</value>
</data>
<data name="fdSaveAs" xml:space="preserve">
<data name="fdSaveAs" xml:space="preserve">
<value>Save as</value>
</data>
<data name="fdOpen" xml:space="preserve">
<data name="fdOpen" xml:space="preserve">
<value>Open</value>
</data>
<data name="fdSelectFolder" xml:space="preserve">
<data name="fdSelectFolder" xml:space="preserve">
<value>Select folder</value>
</data>
<data name="fdSelectMixed" xml:space="preserve">
<data name="fdSelectMixed" xml:space="preserve">
<value>Select Mixed</value>
</data>
<data name="wzBack" xml:space="preserve">
<data name="wzBack" xml:space="preserve">
<value>_Back</value>
</data>
<data name="wzFinish" xml:space="preserve">
<data name="wzFinish" xml:space="preserve">
<value>Fi_nish</value>
</data>
<data name="wzNext" xml:space="preserve">
<data name="wzNext" xml:space="preserve">
<value>_Next...</value>
</data>
<data name="fdDirectoryAlreadyExistsFeedback" xml:space="preserve">
<data name="fdDirectoryAlreadyExistsFeedback" xml:space="preserve">
<value>Directory already exists with that name</value>
<comment>When trying to save a file with a name already taken by a directory</comment>
</data>
<data name="fdDirectoryMustExistFeedback" xml:space="preserve">
<data name="fdDirectoryMustExistFeedback" xml:space="preserve">
<value>Must select an existing directory</value>
</data>
<data name="fdFileAlreadyExistsFeedback" xml:space="preserve">
<data name="fdFileAlreadyExistsFeedback" xml:space="preserve">
<value>File already exists with that name</value>
</data>
<data name="fdFileMustExistFeedback" xml:space="preserve">
<data name="fdFileMustExistFeedback" xml:space="preserve">
<value>Must select an existing file</value>
<comment>When trying to save a directory with a name already used by a file</comment>
</data>
<data name="fdFilename" xml:space="preserve">
<data name="fdFilename" xml:space="preserve">
<value>Filename</value>
</data>
<data name="fdFileOrDirectoryMustExistFeedback" xml:space="preserve">
<data name="fdFileOrDirectoryMustExistFeedback" xml:space="preserve">
<value>Must select an existing file or directory</value>
</data>
<data name="fdModified" xml:space="preserve">
<data name="fdModified" xml:space="preserve">
<value>Modified</value>
</data>
<data name="fdPathCaption" xml:space="preserve">
<data name="fdPathCaption" xml:space="preserve">
<value>Enter Path</value>
</data>
<data name="fdSearchCaption" xml:space="preserve">
<data name="fdSearchCaption" xml:space="preserve">
<value>Enter Search</value>
</data>
<data name="fdSize" xml:space="preserve">
<data name="fdSize" xml:space="preserve">
<value>Size</value>
</data>
<data name="fdType" xml:space="preserve">
<data name="fdType" xml:space="preserve">
<value>Type</value>
</data>
<data name="fdWrongFileTypeFeedback" xml:space="preserve">
<data name="fdWrongFileTypeFeedback" xml:space="preserve">
<value>Wrong file type</value>
<comment>When trying to open/save a file that does not match the provided filter (e.g. csv)</comment>
</data>
<data name="fdAnyFiles" xml:space="preserve">
<data name="fdAnyFiles" xml:space="preserve">
<value>Any Files</value>
<comment>Describes an AllowedType that matches anything</comment>
</data>
<data name="fdDeleteBody" xml:space="preserve">
<data name="fdDeleteBody" xml:space="preserve">
<value>Are you sure you want to delete '{0}'? This operation is permanent</value>
</data>
<data name="fdDeleteFailedTitle" xml:space="preserve">
<data name="fdDeleteFailedTitle" xml:space="preserve">
<value>Delete Failed</value>
</data>
<data name="fdDeleteTitle" xml:space="preserve">
<data name="fdDeleteTitle" xml:space="preserve">
<value>Delete {0}</value>
</data>
<data name="fdNewFailed" xml:space="preserve">
<data name="fdNewFailed" xml:space="preserve">
<value>New Failed</value>
</data>
<data name="fdNewTitle" xml:space="preserve">
<data name="fdNewTitle" xml:space="preserve">
<value>New Folder</value>
</data>
<data name="btnNo" xml:space="preserve">
<data name="btnNo" xml:space="preserve">
<value>_No</value>
</data>
<data name="fdRenameFailedTitle" xml:space="preserve">
<data name="fdRenameFailedTitle" xml:space="preserve">
<value>Rename Failed</value>
</data>
<data name="fdRenamePrompt" xml:space="preserve">
<data name="fdRenamePrompt" xml:space="preserve">
<value>Name:</value>
</data>
<data name="fdRenameTitle" xml:space="preserve">
<data name="fdRenameTitle" xml:space="preserve">
<value>Rename</value>
</data>
<data name="btnYes" xml:space="preserve">
<data name="btnYes" xml:space="preserve">
<value>_Yes</value>
</data>
<data name="fdExisting" xml:space="preserve">
<data name="fdExisting" xml:space="preserve">
<value>Existing</value>
</data>
<data name="btnOpen" xml:space="preserve">
<data name="btnOpen" xml:space="preserve">
<value>O_pen</value>
</data>
<data name="btnSave" xml:space="preserve">
<data name="btnSave" xml:space="preserve">
<value>_Save</value>
</data>
<data name="btnSaveAs" xml:space="preserve">
<data name="btnSaveAs" xml:space="preserve">
<value>Save _as</value>
</data>
<data name="btnOk" xml:space="preserve">
<data name="btnOk" xml:space="preserve">
<value>_OK</value>
</data>
<data name="btnCancel" xml:space="preserve">
<data name="btnCancel" xml:space="preserve">
<value>_Cancel</value>
</data>
<data name="fdCtxDelete" xml:space="preserve">
<data name="fdCtxDelete" xml:space="preserve">
<value>_Delete</value>
</data>
<data name="fdCtxHide" xml:space="preserve">
<data name="fdCtxHide" xml:space="preserve">
<value>_Hide {0}</value>
</data>
<data name="fdCtxNew" xml:space="preserve">
<data name="fdCtxNew" xml:space="preserve">
<value>_New</value>
</data>
<data name="fdCtxRename" xml:space="preserve">
<data name="fdCtxRename" xml:space="preserve">
<value>_Rename</value>
</data>
<data name="fdCtxSortAsc" xml:space="preserve">
<data name="fdCtxSortAsc" xml:space="preserve">
<value>_Sort {0} ASC</value>
</data>
<data name="fdCtxSortDesc" xml:space="preserve">
<data name="fdCtxSortDesc" xml:space="preserve">
<value>_Sort {0} DESC</value>
</data>
<data name="dpTitle" xml:space="preserve">
<data name="dpTitle" xml:space="preserve">
<value>Date Picker</value>
</data>
<data name="#F0F8FF" xml:space="preserve">
<value>AliceBlue</value>
</data>
<data name="#FAEBD7" xml:space="preserve">
<value>AntiqueWhite</value>
</data>
<data name="#7FFFD4" xml:space="preserve">
<value>Aquamarine</value>
</data>
<data name="#F0FFFF" xml:space="preserve">
<value>Azure</value>
</data>
<data name="#F5F5DC" xml:space="preserve">
<value>Beige</value>
</data>
<data name="#FFE4C4" xml:space="preserve">
<value>Bisque</value>
</data>
<data name="#000000" xml:space="preserve">
<value>Black</value>
</data>
<data name="#FFEBCD" xml:space="preserve">
<value>BlanchedAlmond</value>
</data>
<data name="#0000FF" xml:space="preserve">
<value>Blue</value>
</data>
<data name="#8A2BE2" xml:space="preserve">
<value>BlueViolet</value>
</data>
<data name="#A52A2A" xml:space="preserve">
<value>Brown</value>
</data>
<data name="#DEB887" xml:space="preserve">
<value>BurlyWood</value>
</data>
<data name="#5F9EA0" xml:space="preserve">
<value>CadetBlue</value>
</data>
<data name="#7FFF00" xml:space="preserve">
<value>Chartreuse</value>
</data>
<data name="#D2691E" xml:space="preserve">
<value>Chocolate</value>
</data>
<data name="#FF7F50" xml:space="preserve">
<value>Coral</value>
</data>
<data name="#6495ED" xml:space="preserve">
<value>CornflowerBlue</value>
</data>
<data name="#FFF8DC" xml:space="preserve">
<value>Cornsilk</value>
</data>
<data name="#DC143C" xml:space="preserve">
<value>Crimson</value>
</data>
<data name="#00FFFF" xml:space="preserve">
<value>Cyan</value>
</data>
<data name="#00008B" xml:space="preserve">
<value>DarkBlue</value>
</data>
<data name="#008B8B" xml:space="preserve">
<value>DarkCyan</value>
</data>
<data name="#B8860B" xml:space="preserve">
<value>DarkGoldenRod</value>
</data>
<data name="#A9A9A9" xml:space="preserve">
<value>DarkGrey</value>
</data>
<data name="#006400" xml:space="preserve">
<value>DarkGreen</value>
</data>
<data name="#BDB76B" xml:space="preserve">
<value>DarkKhaki</value>
</data>
<data name="#8B008B" xml:space="preserve">
<value>DarkMagenta</value>
</data>
<data name="#556B2F" xml:space="preserve">
<value>DarkOliveGreen</value>
</data>
<data name="#FF8C00" xml:space="preserve">
<value>DarkOrange</value>
</data>
<data name="#9932CC" xml:space="preserve">
<value>DarkOrchid</value>
</data>
<data name="#8B0000" xml:space="preserve">
<value>DarkRed</value>
</data>
<data name="#E9967A" xml:space="preserve">
<value>DarkSalmon</value>
</data>
<data name="#8FBC8F" xml:space="preserve">
<value>DarkSeaGreen</value>
</data>
<data name="#483D8B" xml:space="preserve">
<value>DarkSlateBlue</value>
</data>
<data name="#2F4F4F" xml:space="preserve">
<value>DarkSlateGrey</value>
</data>
<data name="#00CED1" xml:space="preserve">
<value>DarkTurquoise</value>
</data>
<data name="#9400D3" xml:space="preserve">
<value>DarkViolet</value>
</data>
<data name="#FF1493" xml:space="preserve">
<value>DeepPink</value>
</data>
<data name="#00BFFF" xml:space="preserve">
<value>DeepSkyBlue</value>
</data>
<data name="#696969" xml:space="preserve">
<value>DimGray</value>
</data>
<data name="#1E90FF" xml:space="preserve">
<value>DodgerBlue</value>
</data>
<data name="#B22222" xml:space="preserve">
<value>FireBrick</value>
</data>
<data name="#FFFAF0" xml:space="preserve">
<value>FloralWhite</value>
</data>
<data name="#228B22" xml:space="preserve">
<value>ForestGreen</value>
</data>
<data name="#DCDCDC" xml:space="preserve">
<value>Gainsboro</value>
</data>
<data name="#F8F8FF" xml:space="preserve">
<value>GhostWhite</value>
</data>
<data name="#FFD700" xml:space="preserve">
<value>Gold</value>
</data>
<data name="#DAA520" xml:space="preserve">
<value>GoldenRod</value>
</data>
<data name="#808080" xml:space="preserve">
<value>Gray</value>
</data>
<data name="#008000" xml:space="preserve">
<value>Green</value>
</data>
<data name="#ADFF2F" xml:space="preserve">
<value>GreenYellow</value>
</data>
<data name="#F0FFF0" xml:space="preserve">
<value>HoneyDew</value>
</data>
<data name="#FF69B4" xml:space="preserve">
<value>HotPink</value>
</data>
<data name="#CD5C5C" xml:space="preserve">
<value>IndianRed</value>
</data>
<data name="#4B0082" xml:space="preserve">
<value>Indigo</value>
</data>
<data name="#FFFFF0" xml:space="preserve">
<value>Ivory</value>
</data>
<data name="#F0E68C" xml:space="preserve">
<value>Khaki</value>
</data>
<data name="#E6E6FA" xml:space="preserve">
<value>Lavender</value>
</data>
<data name="#FFF0F5" xml:space="preserve">
<value>LavenderBlush</value>
</data>
<data name="#7CFC00" xml:space="preserve">
<value>LawnGreen</value>
</data>
<data name="#FFFACD" xml:space="preserve">
<value>LemonChiffon</value>
</data>
<data name="#ADD8E6" xml:space="preserve">
<value>LightBlue</value>
</data>
<data name="#F08080" xml:space="preserve">
<value>LightCoral</value>
</data>
<data name="#E0FFFF" xml:space="preserve">
<value>LightCyan</value>
</data>
<data name="#FAFAD2" xml:space="preserve">
<value>LightGoldenRodYellow</value>
</data>
<data name="#D3D3D3" xml:space="preserve">
<value>LightGray</value>
</data>
<data name="#90EE90" xml:space="preserve">
<value>LightGreen</value>
</data>
<data name="#FFB6C1" xml:space="preserve">
<value>LightPink</value>
</data>
<data name="#FFA07A" xml:space="preserve">
<value>LightSalmon</value>
</data>
<data name="#20B2AA" xml:space="preserve">
<value>LightSeaGreen</value>
</data>
<data name="#87CEFA" xml:space="preserve">
<value>LightSkyBlue</value>
</data>
<data name="#778899" xml:space="preserve">
<value>LightSlateGrey</value>
</data>
<data name="#B0C4DE" xml:space="preserve">
<value>LightSteelBlue</value>
</data>
<data name="#FFFFE0" xml:space="preserve">
<value>LightYellow</value>
</data>
<data name="#00FF00" xml:space="preserve">
<value>Lime</value>
</data>
<data name="#32CD32" xml:space="preserve">
<value>LimeGreen</value>
</data>
<data name="#FAF0E6" xml:space="preserve">
<value>Linen</value>
</data>
<data name="#FF00FF" xml:space="preserve">
<value>Magenta</value>
</data>
<data name="#800000" xml:space="preserve">
<value>Maroon</value>
</data>
<data name="#66CDAA" xml:space="preserve">
<value>MediumAquaMarine</value>
</data>
<data name="#0000CD" xml:space="preserve">
<value>MediumBlue</value>
</data>
<data name="#BA55D3" xml:space="preserve">
<value>MediumOrchid</value>
</data>
<data name="#9370DB" xml:space="preserve">
<value>MediumPurple</value>
</data>
<data name="#3CB371" xml:space="preserve">
<value>MediumSeaGreen</value>
</data>
<data name="#7B68EE" xml:space="preserve">
<value>MediumSlateBlue</value>
</data>
<data name="#00FA9A" xml:space="preserve">
<value>MediumSpringGreen</value>
</data>
<data name="#48D1CC" xml:space="preserve">
<value>MediumTurquoise</value>
</data>
<data name="#C71585" xml:space="preserve">
<value>MediumVioletRed</value>
</data>
<data name="#191970" xml:space="preserve">
<value>MidnightBlue</value>
</data>
<data name="#F5FFFA" xml:space="preserve">
<value>MintCream</value>
</data>
<data name="#FFE4E1" xml:space="preserve">
<value>MistyRose</value>
</data>
<data name="#FFE4B5" xml:space="preserve">
<value>Moccasin</value>
</data>
<data name="#FFDEAD" xml:space="preserve">
<value>NavajoWhite</value>
</data>
<data name="#000080" xml:space="preserve">
<value>Navy</value>
</data>
<data name="#FDF5E6" xml:space="preserve">
<value>OldLace</value>
</data>
<data name="#808000" xml:space="preserve">
<value>Olive</value>
</data>
<data name="#6B8E23" xml:space="preserve">
<value>OliveDrab</value>
</data>
<data name="#FFA500" xml:space="preserve">
<value>Orange</value>
</data>
<data name="#FF4500" xml:space="preserve">
<value>OrangeRed</value>
</data>
<data name="#DA70D6" xml:space="preserve">
<value>Orchid</value>
</data>
<data name="#EEE8AA" xml:space="preserve">
<value>PaleGoldenRod</value>
</data>
<data name="#98FB98" xml:space="preserve">
<value>PaleGreen</value>
</data>
<data name="#AFEEEE" xml:space="preserve">
<value>PaleTurquoise</value>
</data>
<data name="#DB7093" xml:space="preserve">
<value>PaleVioletRed</value>
</data>
<data name="#FFEFD5" xml:space="preserve">
<value>PapayaWhip</value>
</data>
<data name="#FFDAB9" xml:space="preserve">
<value>PeachPuff</value>
</data>
<data name="#CD853F" xml:space="preserve">
<value>Peru</value>
</data>
<data name="#FFC0CB" xml:space="preserve">
<value>Pink</value>
</data>
<data name="#DDA0DD" xml:space="preserve">
<value>Plum</value>
</data>
<data name="#B0E0E6" xml:space="preserve">
<value>PowderBlue</value>
</data>
<data name="#800080" xml:space="preserve">
<value>Purple</value>
</data>
<data name="#663399" xml:space="preserve">
<value>RebeccaPurple</value>
</data>
<data name="#FF0000" xml:space="preserve">
<value>Red</value>
</data>
<data name="#BC8F8F" xml:space="preserve">
<value>RosyBrown</value>
</data>
<data name="#4169E1" xml:space="preserve">
<value>RoyalBlue</value>
</data>
<data name="#8B4513" xml:space="preserve">
<value>SaddleBrown</value>
</data>
<data name="#FA8072" xml:space="preserve">
<value>Salmon</value>
</data>
<data name="#F4A460" xml:space="preserve">
<value>SandyBrown</value>
</data>
<data name="#2E8B57" xml:space="preserve">
<value>SeaGreen</value>
</data>
<data name="#FFF5EE" xml:space="preserve">
<value>SeaShell</value>
</data>
<data name="#A0522D" xml:space="preserve">
<value>Sienna</value>
</data>
<data name="#C0C0C0" xml:space="preserve">
<value>Silver</value>
</data>
<data name="#87CEEB" xml:space="preserve">
<value>SkyBlue</value>
</data>
<data name="#6A5ACD" xml:space="preserve">
<value>SlateBlue</value>
</data>
<data name="#708090" xml:space="preserve">
<value>SlateGray</value>
</data>
<data name="#FFFAFA" xml:space="preserve">
<value>Snow</value>
</data>
<data name="#00FF7F" xml:space="preserve">
<value>SpringGreen</value>
</data>
<data name="#4682B4" xml:space="preserve">
<value>SteelBlue</value>
</data>
<data name="#D2B48C" xml:space="preserve">
<value>Tan</value>
</data>
<data name="#008080" xml:space="preserve">
<value>Teal</value>
</data>
<data name="#D8BFD8" xml:space="preserve">
<value>Thistle</value>
</data>
<data name="#FF6347" xml:space="preserve">
<value>Tomato</value>
</data>
<data name="#40E0D0" xml:space="preserve">
<value>Turquoise</value>
</data>
<data name="#EE82EE" xml:space="preserve">
<value>Violet</value>
</data>
<data name="#F5DEB3" xml:space="preserve">
<value>Wheat</value>
</data>
<data name="#FFFFFF" xml:space="preserve">
<value>White</value>
</data>
<data name="#F5F5F5" xml:space="preserve">
<value>WhiteSmoke</value>
</data>
<data name="#FFFF00" xml:space="preserve">
<value>Yellow</value>
</data>
<data name="#9ACD32" xml:space="preserve">
<value>YellowGreen</value>
</data>
</root>

View File

@@ -0,0 +1,27 @@
#nullable enable
using ColorHelper;
namespace Terminal.Gui;
internal class BBar : ColorBar
{
public GBar? GBar { get; set; }
public RBar? RBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (RBar == null || GBar == null)
{
throw new ($"{nameof (BBar)} has not been set up correctly before drawing");
}
var rgb = new RGB ((byte)RBar.Value, (byte)GBar.Value, (byte)(MaxValue * fraction));
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 255;
}

View File

@@ -0,0 +1,235 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>
/// A bar representing a single component of a <see cref="Color"/> e.g.
/// the Red portion of a <see cref="ColorModel.RGB"/>.
/// </summary>
internal abstract class ColorBar : View, IColorBar
{
/// <summary>
/// Creates a new instance of the <see cref="ColorBar"/> class.
/// </summary>
protected ColorBar ()
{
Height = 1;
Width = Dim.Fill ();
CanFocus = true;
AddCommand (Command.Left, _ => Adjust (-1));
AddCommand (Command.Right, _ => Adjust (1));
AddCommand (Command.LeftExtend, _ => Adjust (-MaxValue / 20));
AddCommand (Command.RightExtend, _ => Adjust (MaxValue / 20));
AddCommand (Command.LeftHome, _ => SetZero ());
AddCommand (Command.RightEnd, _ => SetMax ());
KeyBindings.Add (Key.CursorLeft, Command.Left);
KeyBindings.Add (Key.CursorRight, Command.Right);
KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend);
KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend);
KeyBindings.Add (Key.Home, Command.LeftHome);
KeyBindings.Add (Key.End, Command.RightEnd);
}
/// <summary>
/// X coordinate that the bar starts at excluding any label.
/// </summary>
private int _barStartsAt;
/// <summary>
/// 0-1 for how much of the color element is present currently (HSL)
/// </summary>
private int _value;
/// <summary>
/// The amount of <see cref="Value"/> represented by each cell width on the bar
/// Can be less than 1 e.g. if Saturation (0-100) and width > 100
/// </summary>
private double _cellValue = 1d;
/// <summary>
/// Last known width of the bar as passed to <see cref="DrawBar"/>.
/// </summary>
private int _barWidth;
/// <summary>
/// The currently selected amount of the color component stored by this class e.g.
/// the amount of Hue in a <see cref="ColorModel.HSL"/>.
/// </summary>
public int Value
{
get => _value;
set
{
int clampedValue = Math.Clamp (value, 0, MaxValue);
if (_value != clampedValue)
{
_value = clampedValue;
OnValueChanged ();
}
}
}
/// <inheritdoc/>
void IColorBar.SetValueWithoutRaisingEvent (int v)
{
_value = v;
SetNeedsDisplay ();
}
/// <inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
var xOffset = 0;
if (!string.IsNullOrWhiteSpace (Text))
{
Move (0, 0);
Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
Driver.AddStr (Text);
// TODO: is there a better method than this? this is what it is in TableView
xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ());
}
_barWidth = viewport.Width - xOffset;
_barStartsAt = xOffset;
DrawBar (xOffset, 0, _barWidth);
}
/// <summary>
/// Event fired when <see cref="Value"/> is changed to a new value
/// </summary>
public event EventHandler<EventArgs<int>>? ValueChanged;
/// <inheritdoc/>
protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
{
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
{
if (mouseEvent.Position.X >= _barStartsAt)
{
double v = MaxValue * ((double)mouseEvent.Position.X - _barStartsAt) / (_barWidth - 1);
Value = Math.Clamp ((int)v, 0, MaxValue);
}
mouseEvent.Handled = true;
FocusFirst (null);
return true;
}
return base.OnMouseEvent (mouseEvent);
}
/// <summary>
/// When overriden in a derived class, returns the <see cref="Color"/> to
/// render at <paramref name="fraction"/> proportion of the full bars width.
/// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100.
/// </summary>
/// <param name="fraction"></param>
/// <returns></returns>
protected abstract Color GetColor (double fraction);
/// <summary>
/// The maximum value allowed for this component e.g. Saturation allows up to 100 as it
/// is a percentage while Hue allows up to 360 as it is measured in degrees.
/// </summary>
protected abstract int MaxValue { get; }
/// <summary>
/// The last drawn location in View's viewport where the Triangle appeared.
/// Used exclusively for tests.
/// </summary>
internal int TrianglePosition { get; private set; }
private bool? Adjust (int delta)
{
var change = (int)(delta * _cellValue);
// Ensure that the change is at least 1 or -1 if delta is non-zero
if (change == 0 && delta != 0)
{
change = delta > 0 ? 1 : -1;
}
Value += change;
return true;
}
private void DrawBar (int xOffset, int yOffset, int width)
{
// Each 1 unit of X in the bar corresponds to this much of Value
_cellValue = (double)MaxValue / (width - 1);
for (var x = 0; x < width; x++)
{
double fraction = (double)x / (width - 1);
Color color = GetColor (fraction);
// Adjusted isSelectedCell calculation
double cellBottomThreshold = (x - 1) * _cellValue;
double cellTopThreshold = x * _cellValue;
if (x == width - 1)
{
cellTopThreshold = MaxValue;
}
bool isSelectedCell = Value > cellBottomThreshold && Value <= cellTopThreshold;
// Check the brightness of the background color
double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
Color triangleColor = Color.Black;
if (brightness < 0.15) // Threshold to determine if the color is too close to black
{
triangleColor = Color.DarkGray;
}
if (isSelectedCell)
{
// Draw the triangle at the closest position
Application.Driver?.SetAttribute (new (triangleColor, color));
AddRune (x + xOffset, yOffset, new ('▲'));
// Record for tests
TrianglePosition = x + xOffset;
}
else
{
Application.Driver?.SetAttribute (new (color, color));
AddRune (x + xOffset, yOffset, new ('█'));
}
}
}
private void OnValueChanged ()
{
ValueChanged?.Invoke (this, new (in _value));
SetNeedsDisplay ();
}
private bool? SetMax ()
{
Value = MaxValue;
return true;
}
private bool? SetZero ()
{
Value = 0;
return true;
}
}

View File

@@ -0,0 +1,163 @@
#nullable enable
using ColorHelper;
using ColorConverter = ColorHelper.ColorConverter;
namespace Terminal.Gui;
internal class ColorModelStrategy
{
public IEnumerable<ColorBar> CreateBars (ColorModel model)
{
switch (model)
{
case ColorModel.RGB:
return CreateRgbBars ();
case ColorModel.HSV:
return CreateHsvBars ();
case ColorModel.HSL:
return CreateHslBars ();
default:
throw new ArgumentOutOfRangeException (nameof (model), model, null);
}
}
public Color GetColorFromBars (IList<IColorBar> bars, ColorModel model)
{
switch (model)
{
case ColorModel.RGB:
return ToColor (new ((byte)bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value));
case ColorModel.HSV:
return ToColor (
ColorConverter.HsvToRgb (new (bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value))
);
case ColorModel.HSL:
return ToColor (
ColorConverter.HslToRgb (new (bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value))
);
default:
throw new ArgumentOutOfRangeException (nameof (model), model, null);
}
}
public void SetBarsToColor (IList<IColorBar> bars, Color newValue, ColorModel model)
{
switch (model)
{
case ColorModel.RGB:
bars [0].SetValueWithoutRaisingEvent (newValue.R);
bars [1].SetValueWithoutRaisingEvent (newValue.G);
bars [2].SetValueWithoutRaisingEvent (newValue.B);
break;
case ColorModel.HSV:
HSV newHsv = ColorConverter.RgbToHsv (new (newValue.R, newValue.G, newValue.B));
bars [0].SetValueWithoutRaisingEvent (newHsv.H);
bars [1].SetValueWithoutRaisingEvent (newHsv.S);
bars [2].SetValueWithoutRaisingEvent (newHsv.V);
break;
case ColorModel.HSL:
HSL newHsl = ColorConverter.RgbToHsl (new (newValue.R, newValue.G, newValue.B));
bars [0].SetValueWithoutRaisingEvent (newHsl.H);
bars [1].SetValueWithoutRaisingEvent (newHsl.S);
bars [2].SetValueWithoutRaisingEvent (newHsl.L);
break;
default:
throw new ArgumentOutOfRangeException (nameof (model), model, null);
}
}
private IEnumerable<ColorBar> CreateHslBars ()
{
var h = new HueBar
{
Text = "H:"
};
yield return h;
var s = new SaturationBar
{
Text = "S:"
};
var l = new LightnessBar
{
Text = "L:"
};
s.HBar = h;
s.LBar = l;
l.HBar = h;
l.SBar = s;
yield return s;
yield return l;
}
private IEnumerable<ColorBar> CreateHsvBars ()
{
var h = new HueBar
{
Text = "H:"
};
yield return h;
var s = new SaturationBar
{
Text = "S:"
};
var v = new ValueBar
{
Text = "V:"
};
s.HBar = h;
s.VBar = v;
v.HBar = h;
v.SBar = s;
yield return s;
yield return v;
}
private IEnumerable<ColorBar> CreateRgbBars ()
{
var r = new RBar
{
Text = "R:"
};
var g = new GBar
{
Text = "G:"
};
var b = new BBar
{
Text = "B:"
};
r.GBar = g;
r.BBar = b;
g.RBar = r;
g.BBar = b;
b.RBar = r;
b.GBar = g;
yield return r;
yield return g;
yield return b;
}
private Color ToColor (RGB rgb) { return new (rgb.R, rgb.G, rgb.B); }
}

View File

@@ -1,279 +1,318 @@
namespace Terminal.Gui;
#nullable enable
/// <summary>Event arguments for the <see cref="Color"/> events.</summary>
public class ColorEventArgs : EventArgs
{
/// <summary>Initializes a new instance of <see cref="ColorEventArgs"/></summary>
public ColorEventArgs () { }
namespace Terminal.Gui;
/// <summary>The new Thickness.</summary>
public Color Color { get; set; }
/// <summary>The previous Thickness.</summary>
public Color PreviousColor { get; set; }
}
/// <summary>The <see cref="ColorPicker"/> <see cref="View"/> Color picker.</summary>
/// <summary>
/// True color picker using HSL
/// </summary>
public class ColorPicker : View
{
/// <summary>Columns of color boxes</summary>
private readonly int _cols = 8;
/// <summary>Rows of color boxes</summary>
private readonly int _rows = 2;
private int _boxHeight = 2;
private int _boxWidth = 4;
private int _selectColorIndex = (int)Color.Black;
/// <summary>Initializes a new instance of <see cref="ColorPicker"/>.</summary>
public ColorPicker () { SetInitialProperties (); }
private void SetInitialProperties ()
/// <summary>
/// Creates a new instance of <see cref="ColorPicker"/>. Use
/// <see cref="Style"/> to change color model. Use <see cref="SelectedColor"/>
/// to change initial <see cref="Color"/>.
/// </summary>
public ColorPicker ()
{
HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed;
CanFocus = true;
AddCommands ();
AddKeyBindings ();
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
MouseClick += ColorPicker_MouseClick;
Height = Dim.Auto ();
Width = Dim.Auto ();
ApplyStyleChanges ();
}
// TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor.
private readonly Dictionary<IColorBar, TextField> _textFields = new ();
private readonly ColorModelStrategy _strategy = new ();
private TextField? _tfHex;
private Label? _lbHex;
private void ColorPicker_MouseClick (object sender, MouseEventEventArgs me)
{
// if (CanFocus)
{
Cursor = new Point (me.MouseEvent.Position.X / _boxWidth, me.MouseEvent.Position.Y / _boxHeight);
SetFocus ();
me.Handled = true;
}
}
private TextField? _tfName;
private Label? _lbName;
/// <summary>Height of a color box</summary>
public int BoxHeight
private Color _selectedColor = Color.Black;
// TODO: Add interface
private readonly IColorNameResolver _colorNameResolver = new W3CColors ();
private List<IColorBar> _bars = new ();
/// <summary>
/// Rebuild the user interface to reflect the new state of <see cref="Style"/>.
/// </summary>
public void ApplyStyleChanges ()
{
get => _boxHeight;
set
Color oldValue = _selectedColor;
DisposeOldViews ();
var y = 0;
const int textFieldWidth = 4;
foreach (ColorBar bar in _strategy.CreateBars (Style.ColorModel))
{
if (_boxHeight != value)
bar.Y = y;
bar.Width = Dim.Fill (Style.ShowTextFields ? textFieldWidth : 0);
if (Style.ShowTextFields)
{
_boxHeight = value;
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
SetNeedsLayout ();
var tfValue = new TextField
{
X = Pos.AnchorEnd (textFieldWidth),
Y = y,
Width = textFieldWidth
};
tfValue.Leave += UpdateSingleBarValueFromTextField;
_textFields.Add (bar, tfValue);
Add (tfValue);
}
}
}
/// <summary>Width of a color box</summary>
public int BoxWidth
{
get => _boxWidth;
set
y++;
bar.ValueChanged += RebuildColorFromBar;
_bars.Add (bar);
Add (bar);
}
if (Style.ShowColorName)
{
if (_boxWidth != value)
{
_boxWidth = value;
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
SetNeedsLayout ();
}
}
}
/// <summary>Cursor for the selected color.</summary>
public Point Cursor
{
get => new (_selectColorIndex % _cols, _selectColorIndex / _cols);
set
{
int colorIndex = value.Y * _cols + value.X;
SelectedColor = (ColorName)colorIndex;
}
}
/// <summary>Selected color.</summary>
public ColorName SelectedColor
{
get => (ColorName)_selectColorIndex;
set
{
if (value == (ColorName)_selectColorIndex)
{
return;
}
var prev = (ColorName)_selectColorIndex;
_selectColorIndex = (int)value;
ColorChanged?.Invoke (
this,
new ColorEventArgs { PreviousColor = new Color (prev), Color = new Color (value) }
);
SetNeedsDisplay ();
}
}
/// <summary>Fired when a color is picked.</summary>
public event EventHandler<ColorEventArgs> ColorChanged;
/// <summary>Moves the selected item index to the previous column.</summary>
/// <returns></returns>
public virtual bool MoveLeft ()
{
if (Cursor.X > 0)
{
SelectedColor--;
CreateNameField ();
}
return true;
CreateTextField ();
SelectedColor = oldValue;
LayoutSubviews ();
}
/// <summary>Moves the selected item index to the next column.</summary>
/// <returns></returns>
public virtual bool MoveRight ()
{
if (Cursor.X < _cols - 1)
{
SelectedColor++;
}
/// <summary>
/// Fired when color is changed.
/// </summary>
public event EventHandler<ColorEventArgs>? ColorChanged;
return true;
}
/// <summary>Moves the selected item index to the previous row.</summary>
/// <returns></returns>
public virtual bool MoveUp ()
{
if (Cursor.Y > 0)
{
SelectedColor -= _cols;
}
return true;
}
/// <summary>Moves the selected item index to the next row.</summary>
/// <returns></returns>
public virtual bool MoveDown ()
{
if (Cursor.Y < _rows - 1)
{
SelectedColor += _cols;
}
return true;
}
///<inheritdoc/>
/// <inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
Attribute normal = GetNormalColor ();
Driver.SetAttribute (new (SelectedColor, normal.Background));
int y = _bars.Count + (Style.ShowColorName ? 1 : 0);
AddRune (13, y, (Rune)'■');
}
Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ());
var colorIndex = 0;
/// <summary>
/// The color selected in the picker
/// </summary>
public Color SelectedColor
{
get => _selectedColor;
set => SetSelectedColor (value, true);
}
for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++)
/// <summary>
/// Style settings for the color picker. After making changes ensure you call
/// <see cref="ApplyStyleChanges"/>.
/// </summary>
public ColorPickerStyle Style { get; set; } = new ();
private void CreateNameField ()
{
_lbName = new ()
{
for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++)
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.Leave += 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.Leave += UpdateValueFromTextField;
}
private void DisposeOldViews ()
{
foreach (ColorBar bar in _bars.Cast<ColorBar> ())
{
bar.ValueChanged -= RebuildColorFromBar;
if (_textFields.TryGetValue (bar, out TextField? tf))
{
int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols;
Driver.SetAttribute (new Attribute ((ColorName)foregroundColorIndex, (ColorName)colorIndex));
bool selected = x == Cursor.X && y == Cursor.Y;
DrawColorBox (x, y, selected);
colorIndex++;
tf.Leave -= UpdateSingleBarValueFromTextField;
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.Leave -= UpdateValueFromTextField;
_tfHex.Dispose ();
_tfHex = null;
}
if (_lbName != null)
{
Remove (_lbName);
_lbName.Dispose ();
_lbName = null;
}
if (_tfName != null)
{
Remove (_tfName);
_tfName.Leave -= UpdateValueFromName;
_tfName.Dispose ();
_tfName = null;
}
}
private void RebuildColorFromBar (object? sender, EventArgs<int> 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<IColorBar, TextField> 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;
}
}
private void UpdateSingleBarValueFromTextField (object? sender, FocusEventArgs e)
{
foreach (KeyValuePair<IColorBar, TextField> kvp in _textFields)
{
if (kvp.Value == sender)
{
if (int.TryParse (kvp.Value.Text, out int v))
{
kvp.Key.Value = v;
}
}
}
}
/// <summary>Add the commands.</summary>
private void AddCommands ()
private void UpdateValueFromName (object? sender, FocusEventArgs e)
{
AddCommand (Command.Left, () => MoveLeft ());
AddCommand (Command.Right, () => MoveRight ());
AddCommand (Command.LineUp, () => MoveUp ());
AddCommand (Command.LineDown, () => MoveDown ());
}
/// <summary>Add the KeyBindinds.</summary>
private void AddKeyBindings ()
{
KeyBindings.Add (Key.CursorLeft, Command.Left);
KeyBindings.Add (Key.CursorRight, Command.Right);
KeyBindings.Add (Key.CursorUp, Command.LineUp);
KeyBindings.Add (Key.CursorDown, Command.LineDown);
}
/// <summary>Draw a box for one color.</summary>
/// <param name="x">X location.</param>
/// <param name="y">Y location</param>
/// <param name="selected"></param>
private void DrawColorBox (int x, int y, bool selected)
{
var index = 0;
for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++)
if (_tfName == null)
{
for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++)
{
Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY);
Driver.AddRune ((Rune)' ');
index++;
}
return;
}
if (selected)
if (_colorNameResolver.TryParseColor (_tfName.Text, out Color newColor))
{
DrawFocusRect (new (x * BoxWidth, y * BoxHeight, BoxWidth, BoxHeight));
}
}
private void DrawFocusRect (Rectangle rect)
{
var lc = new LineCanvas ();
if (rect.Width == 1)
{
lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
}
else if (rect.Height == 1)
{
lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
SelectedColor = newColor;
}
else
{
lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
// value is invalid, revert the value in the text field back to current state
SyncSubViewValues (false);
}
}
lc.AddLine (
rect.Location with { Y = rect.Location.Y + rect.Height - 1 },
rect.Width,
Orientation.Horizontal,
LineStyle.Dotted
);
lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
lc.AddLine (
rect.Location with { X = rect.Location.X + rect.Width - 1 },
rect.Height,
Orientation.Vertical,
LineStyle.Dotted
);
private void UpdateValueFromTextField (object? sender, FocusEventArgs e)
{
if (_tfHex == null)
{
return;
}
foreach (KeyValuePair<Point, Rune> p in lc.GetMap ())
if (Color.TryParse (_tfHex.Text, out Color? newColor))
{
AddRune (p.Key.X, p.Key.Y, p.Value);
SelectedColor = newColor.Value;
}
else
{
// value is invalid, revert the value in the text field back to current state
SyncSubViewValues (false);
}
}
}

View File

@@ -0,0 +1,270 @@
namespace Terminal.Gui;
/// <summary>The <see cref="ColorPicker16"/> <see cref="View"/> Color picker.</summary>
public class ColorPicker16 : View
{
/// <summary>Initializes a new instance of <see cref="ColorPicker16"/>.</summary>
public ColorPicker16 () { SetInitialProperties (); }
/// <summary>Columns of color boxes</summary>
private readonly int _cols = 8;
/// <summary>Rows of color boxes</summary>
private readonly int _rows = 2;
private int _boxHeight = 2;
private int _boxWidth = 4;
private int _selectColorIndex = (int)Color.Black;
/// <summary>Height of a color box</summary>
public int BoxHeight
{
get => _boxHeight;
set
{
if (_boxHeight != value)
{
_boxHeight = value;
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
SetNeedsLayout ();
}
}
}
/// <summary>Width of a color box</summary>
public int BoxWidth
{
get => _boxWidth;
set
{
if (_boxWidth != value)
{
_boxWidth = value;
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
SetNeedsLayout ();
}
}
}
/// <summary>Fired when a color is picked.</summary>
public event EventHandler<ColorEventArgs> ColorChanged;
/// <summary>Cursor for the selected color.</summary>
public Point Cursor
{
get => new (_selectColorIndex % _cols, _selectColorIndex / _cols);
set
{
int colorIndex = value.Y * _cols + value.X;
SelectedColor = (ColorName)colorIndex;
}
}
/// <summary>Moves the selected item index to the next row.</summary>
/// <returns></returns>
public virtual bool MoveDown ()
{
if (Cursor.Y < _rows - 1)
{
SelectedColor += _cols;
}
return true;
}
/// <summary>Moves the selected item index to the previous column.</summary>
/// <returns></returns>
public virtual bool MoveLeft ()
{
if (Cursor.X > 0)
{
SelectedColor--;
}
return true;
}
/// <summary>Moves the selected item index to the next column.</summary>
/// <returns></returns>
public virtual bool MoveRight ()
{
if (Cursor.X < _cols - 1)
{
SelectedColor++;
}
return true;
}
/// <summary>Moves the selected item index to the previous row.</summary>
/// <returns></returns>
public virtual bool MoveUp ()
{
if (Cursor.Y > 0)
{
SelectedColor -= _cols;
}
return true;
}
///<inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ());
var colorIndex = 0;
for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++)
{
for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++)
{
int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols;
if (foregroundColorIndex > 15 || colorIndex > 15)
{
continue;
}
Driver.SetAttribute (new ((ColorName)foregroundColorIndex, (ColorName)colorIndex));
bool selected = x == Cursor.X && y == Cursor.Y;
DrawColorBox (x, y, selected);
colorIndex++;
}
}
}
/// <summary>Selected color.</summary>
public ColorName SelectedColor
{
get => (ColorName)_selectColorIndex;
set
{
if (value == (ColorName)_selectColorIndex)
{
return;
}
_selectColorIndex = (int)value;
ColorChanged?.Invoke (
this,
new (value)
);
SetNeedsDisplay ();
}
}
/// <summary>Add the commands.</summary>
private void AddCommands ()
{
AddCommand (Command.Left, () => MoveLeft ());
AddCommand (Command.Right, () => MoveRight ());
AddCommand (Command.LineUp, () => MoveUp ());
AddCommand (Command.LineDown, () => MoveDown ());
}
/// <summary>Add the KeyBindinds.</summary>
private void AddKeyBindings ()
{
KeyBindings.Add (Key.CursorLeft, Command.Left);
KeyBindings.Add (Key.CursorRight, Command.Right);
KeyBindings.Add (Key.CursorUp, Command.LineUp);
KeyBindings.Add (Key.CursorDown, Command.LineDown);
}
// TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor.
private void ColorPicker_MouseClick (object sender, MouseEventEventArgs me)
{
// if (CanFocus)
{
Cursor = new (me.MouseEvent.Position.X / _boxWidth, me.MouseEvent.Position.Y / _boxHeight);
SetFocus ();
me.Handled = true;
}
}
/// <summary>Draw a box for one color.</summary>
/// <param name="x">X location.</param>
/// <param name="y">Y location</param>
/// <param name="selected"></param>
private void DrawColorBox (int x, int y, bool selected)
{
var index = 0;
for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++)
{
for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++)
{
Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY);
Driver.AddRune ((Rune)' ');
index++;
}
}
if (selected)
{
DrawFocusRect (new (x * BoxWidth, y * BoxHeight, BoxWidth, BoxHeight));
}
}
private void DrawFocusRect (Rectangle rect)
{
var lc = new LineCanvas ();
if (rect.Width == 1)
{
lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
}
else if (rect.Height == 1)
{
lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
}
else
{
lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
lc.AddLine (
rect.Location with { Y = rect.Location.Y + rect.Height - 1 },
rect.Width,
Orientation.Horizontal,
LineStyle.Dotted
);
lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
lc.AddLine (
rect.Location with { X = rect.Location.X + rect.Width - 1 },
rect.Height,
Orientation.Vertical,
LineStyle.Dotted
);
}
foreach (KeyValuePair<Point, Rune> p in lc.GetMap ())
{
AddRune (p.Key.X, p.Key.Y, p.Value);
}
}
private void SetInitialProperties ()
{
HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
CanFocus = true;
AddCommands ();
AddKeyBindings ();
Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
MouseClick += ColorPicker_MouseClick;
}
}

View File

@@ -0,0 +1,25 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>
/// Contains style settings for <see cref="ColorPicker"/> e.g. which <see cref="ColorModel"/>
/// to use.
/// </summary>
public class ColorPickerStyle
{
/// <summary>
/// The color model for picking colors by RGB, HSV, etc.
/// </summary>
public ColorModel ColorModel { get; set; } = ColorModel.HSV;
/// <summary>
/// True to put the numerical value of bars on the right of the color bar
/// </summary>
public bool ShowTextFields { get; set; } = true;
/// <summary>
/// True to show an editable text field indicating the w3c/console color name of selected color.
/// </summary>
public bool ShowColorName { get; set; } = false;
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using ColorHelper;
namespace Terminal.Gui;
internal class GBar : ColorBar
{
public BBar? BBar { get; set; }
public RBar? RBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (RBar == null || BBar == null)
{
throw new ($"{nameof (GBar)} has not been set up correctly before drawing");
}
var rgb = new RGB ((byte)RBar.Value, (byte)(MaxValue * fraction), (byte)BBar.Value);
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 255;
}

View File

@@ -0,0 +1,21 @@
#nullable enable
using ColorHelper;
using ColorConverter = ColorHelper.ColorConverter;
namespace Terminal.Gui;
internal class HueBar : ColorBar
{
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
var hsl = new HSL ((int)(MaxValue * fraction), 100, 50);
RGB rgb = ColorConverter.HslToRgb (hsl);
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 360;
}

View File

@@ -0,0 +1,14 @@
namespace Terminal.Gui;
internal interface IColorBar
{
int Value { get; set; }
/// <summary>
/// Update the value of <see cref="Value"/> and reflect
/// changes in UI state but do not raise a value changed
/// event (to avoid circular events).
/// </summary>
/// <param name="v"></param>
internal void SetValueWithoutRaisingEvent (int v);
}

View File

@@ -0,0 +1,29 @@
#nullable enable
using ColorHelper;
using ColorConverter = ColorHelper.ColorConverter;
namespace Terminal.Gui;
internal class LightnessBar : ColorBar
{
public HueBar? HBar { get; set; }
public SaturationBar? SBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (HBar == null || SBar == null)
{
throw new ($"{nameof (LightnessBar)} has not been set up correctly before drawing");
}
var hsl = new HSL (HBar.Value, (byte)SBar.Value, (byte)(MaxValue * fraction));
RGB rgb = ColorConverter.HslToRgb (hsl);
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 100;
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using ColorHelper;
namespace Terminal.Gui;
internal class RBar : ColorBar
{
public BBar? BBar { get; set; }
public GBar? GBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (GBar == null || BBar == null)
{
throw new ($"{nameof (RBar)} has not been set up correctly before drawing");
}
var rgb = new RGB ((byte)(MaxValue * fraction), (byte)GBar.Value, (byte)BBar.Value);
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 255;
}

View File

@@ -0,0 +1,40 @@
#nullable enable
using ColorHelper;
using ColorConverter = ColorHelper.ColorConverter;
namespace Terminal.Gui;
internal class SaturationBar : ColorBar
{
public HueBar? HBar { get; set; }
// Should only have either LBar or VBar not both
public LightnessBar? LBar { get; set; }
public ValueBar? VBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (LBar != null && HBar != null)
{
var hsl = new HSL (HBar.Value, (byte)(MaxValue * fraction), (byte)LBar.Value);
RGB rgb = ColorConverter.HslToRgb (hsl);
return new (rgb.R, rgb.G, rgb.B);
}
if (VBar != null && HBar != null)
{
var hsv = new HSV (HBar.Value, (byte)(MaxValue * fraction), (byte)VBar.Value);
RGB rgb = ColorConverter.HsvToRgb (hsv);
return new (rgb.R, rgb.G, rgb.B);
}
throw new ($"{nameof (SaturationBar)} requires either Lightness or Value to render");
}
/// <inheritdoc/>
protected override int MaxValue => 100;
}

View File

@@ -0,0 +1,29 @@
#nullable enable
using ColorHelper;
using ColorConverter = ColorHelper.ColorConverter;
namespace Terminal.Gui;
internal class ValueBar : ColorBar
{
public HueBar? HBar { get; set; }
public SaturationBar? SBar { get; set; }
/// <inheritdoc/>
protected override Color GetColor (double fraction)
{
if (HBar == null || SBar == null)
{
throw new ($"{nameof (ValueBar)} has not been set up correctly before drawing");
}
var hsv = new HSV (HBar.Value, (byte)SBar.Value, (byte)(MaxValue * fraction));
RGB rgb = ColorConverter.HsvToRgb (hsv);
return new (rgb.R, rgb.G, rgb.B);
}
/// <inheritdoc/>
protected override int MaxValue => 100;
}

View File

@@ -8,7 +8,7 @@ namespace UICatalog.Scenarios;
/// </summary>
public class AdornmentEditor : View
{
private readonly ColorPicker _backgroundColorPicker = new ()
private readonly ColorPicker16 _backgroundColorPicker = new ()
{
Title = "_BG",
BoxWidth = 1,
@@ -18,7 +18,7 @@ public class AdornmentEditor : View
Enabled = false
};
private readonly ColorPicker _foregroundColorPicker = new ()
private readonly ColorPicker16 _foregroundColorPicker = new ()
{
Title = "_FG",
BoxWidth = 1,

View File

@@ -38,7 +38,7 @@ public class Adornments : Scenario
app.Add (window);
var tf1 = new TextField { Width = 10, Text = "TextField" };
var color = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () };
var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () };
color.BorderStyle = LineStyle.RoundedDotted;
color.ColorChanged += (s, e) =>
@@ -47,7 +47,7 @@ public class Adornments : Scenario
{
Normal = new (
color.SuperView.ColorScheme.Normal.Foreground,
e.Color
e.CurrentValue
)
};
};

View File

@@ -23,6 +23,12 @@ public class ColorPickers : Scenario
/// <summary>Foreground ColorPicker.</summary>
private ColorPicker foregroundColorPicker;
/// <summary>Background ColorPicker.</summary>
private ColorPicker16 backgroundColorPicker16;
/// <summary>Foreground ColorPicker.</summary>
private ColorPicker16 foregroundColorPicker16;
/// <summary>Setup the scenario.</summary>
public override void Main ()
{
@@ -33,8 +39,16 @@ public class ColorPickers : Scenario
Title = GetQuitKeyAndName (),
};
///////////////////////////////////////
// True Color Pickers
///////////////////////////////////////
// Foreground ColorPicker.
foregroundColorPicker = new ColorPicker { Title = "Foreground Color", BorderStyle = LineStyle.Single };
foregroundColorPicker = new ColorPicker {
Title = "Foreground Color",
BorderStyle = LineStyle.Single,
Width = Dim.Percent (50)
};
foregroundColorPicker.ColorChanged += ForegroundColor_ColorChanged;
app.Add (foregroundColorPicker);
@@ -49,9 +63,8 @@ public class ColorPickers : Scenario
{
Title = "Background Color",
X = Pos.AnchorEnd (),
BoxHeight = 1,
BoxWidth = 4,
BorderStyle = LineStyle.Single,
Width = Dim.Percent (50),
BorderStyle = LineStyle.Single
};
backgroundColorPicker.ColorChanged += BackgroundColor_ColorChanged;
@@ -64,6 +77,37 @@ public class ColorPickers : Scenario
app.Add (_backgroundColorLabel);
///////////////////////////////////////
// 16 Color Pickers
///////////////////////////////////////
// Foreground ColorPicker 16.
foregroundColorPicker16 = new ColorPicker16
{
Title = "Foreground Color",
BorderStyle = LineStyle.Single,
Width = Dim.Percent (50),
Visible = false // We default to HSV so hide old one
};
foregroundColorPicker16.ColorChanged += ForegroundColor_ColorChanged;
app.Add (foregroundColorPicker16);
// Background ColorPicker 16.
backgroundColorPicker16 = new ColorPicker16
{
Title = "Background Color",
X = Pos.AnchorEnd (),
Width = Dim.Percent (50),
BorderStyle = LineStyle.Single,
Visible = false // We default to HSV so hide old one
};
backgroundColorPicker16.ColorChanged += BackgroundColor_ColorChanged;
app.Add (backgroundColorPicker16);
// Demo Label.
_demoView = new View
{
@@ -79,6 +123,98 @@ public class ColorPickers : Scenario
};
app.Add (_demoView);
// Radio for switching color models
var rgColorModel = new RadioGroup ()
{
Y = Pos.Bottom (_demoView),
Width = Dim.Auto (),
Height = Dim.Auto (),
RadioLabels = new []
{
"RGB",
"HSV",
"HSL",
"16 Colors"
},
SelectedItem = (int)foregroundColorPicker.Style.ColorModel,
};
rgColorModel.SelectedItemChanged += (_, e) =>
{
// 16 colors
if (e.SelectedItem == 3)
{
foregroundColorPicker16.Visible = true;
foregroundColorPicker.Visible = false;
backgroundColorPicker16.Visible = true;
backgroundColorPicker.Visible = false;
// Switching to 16 colors
ForegroundColor_ColorChanged (null,null);
BackgroundColor_ColorChanged (null, null);
}
else
{
foregroundColorPicker16.Visible = false;
foregroundColorPicker.Visible = true;
foregroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem;
foregroundColorPicker.ApplyStyleChanges ();
backgroundColorPicker16.Visible = false;
backgroundColorPicker.Visible = true;
backgroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem;
backgroundColorPicker.ApplyStyleChanges ();
// Switching to true colors
foregroundColorPicker.SelectedColor = foregroundColorPicker16.SelectedColor;
backgroundColorPicker.SelectedColor = backgroundColorPicker16.SelectedColor;
}
};
app.Add (rgColorModel);
// Checkbox for switching show text fields on and off
var cbShowTextFields = new CheckBox ()
{
Text = "Show Text Fields",
Y = Pos.Bottom (rgColorModel)+1,
Width = Dim.Auto (),
Height = Dim.Auto (),
CheckedState = foregroundColorPicker.Style.ShowTextFields ? CheckState.Checked: CheckState.UnChecked,
};
cbShowTextFields.CheckedStateChanging += (_, e) =>
{
foregroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked;
foregroundColorPicker.ApplyStyleChanges ();
backgroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked;
backgroundColorPicker.ApplyStyleChanges ();
};
app.Add (cbShowTextFields);
// Checkbox for switching show text fields on and off
var cbShowName = new CheckBox ()
{
Text = "Show Color Name",
Y = Pos.Bottom (cbShowTextFields) + 1,
Width = Dim.Auto (),
Height = Dim.Auto (),
CheckedState = foregroundColorPicker.Style.ShowColorName ? CheckState.Checked : CheckState.UnChecked,
};
cbShowName.CheckedStateChanging += (_, e) =>
{
foregroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked;
foregroundColorPicker.ApplyStyleChanges ();
backgroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked;
backgroundColorPicker.ApplyStyleChanges ();
};
app.Add (cbShowName);
// Set default colors.
foregroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Foreground.GetClosestNamedColor ();
backgroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Background.GetClosestNamedColor ();
@@ -92,25 +228,32 @@ public class ColorPickers : Scenario
/// <summary>Fired when background color is changed.</summary>
private void BackgroundColor_ColorChanged (object sender, EventArgs e)
{
UpdateColorLabel (_backgroundColorLabel, backgroundColorPicker);
UpdateColorLabel (_backgroundColorLabel,
backgroundColorPicker.Visible ?
backgroundColorPicker.SelectedColor :
backgroundColorPicker16.SelectedColor
);
UpdateDemoLabel ();
}
/// <summary>Fired when foreground color is changed.</summary>
private void ForegroundColor_ColorChanged (object sender, EventArgs e)
{
UpdateColorLabel (_foregroundColorLabel, foregroundColorPicker);
UpdateColorLabel (_foregroundColorLabel,
foregroundColorPicker.Visible ?
foregroundColorPicker.SelectedColor :
foregroundColorPicker16.SelectedColor
);
UpdateDemoLabel ();
}
/// <summary>Update a color label from his ColorPicker.</summary>
private void UpdateColorLabel (Label label, ColorPicker colorPicker)
private void UpdateColorLabel (Label label, Color color)
{
label.Clear ();
var color = new Color (colorPicker.SelectedColor);
label.Text =
$"{colorPicker.SelectedColor} ({(int)colorPicker.SelectedColor}) #{color.R:X2}{color.G:X2}{color.B:X2}";
$"{color} ({(int)color}) #{color.R:X2}{color.G:X2}{color.B:X2}";
}
/// <summary>Update Demo Label.</summary>
@@ -119,8 +262,12 @@ public class ColorPickers : Scenario
_demoView.ColorScheme = new ColorScheme
{
Normal = new Attribute (
foregroundColorPicker.SelectedColor,
backgroundColorPicker.SelectedColor
foregroundColorPicker.Visible ?
foregroundColorPicker.SelectedColor :
foregroundColorPicker16.SelectedColor,
backgroundColorPicker.Visible ?
backgroundColorPicker.SelectedColor :
backgroundColorPicker16.SelectedColor
)
};
}

View File

@@ -326,7 +326,7 @@ public class ContentScrolling : Scenario
// Add demo views to show that things work correctly
var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test TextField" };
var colorPicker = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 };
var colorPicker = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 };
colorPicker.BorderStyle = LineStyle.RoundedDotted;
colorPicker.ColorChanged += (s, e) =>
@@ -335,7 +335,7 @@ public class ContentScrolling : Scenario
{
Normal = new (
colorPicker.SuperView.ColorScheme.Normal.Foreground,
e.Color
e.CurrentValue
)
};
};

View File

@@ -1,10 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Terminal.Gui;
namespace UICatalog.Scenarios;
public interface ITool
{
void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent);
}
internal class DrawLineTool : ITool
{
private StraightLine _currentLine;
public LineStyle LineStyle { get; set; } = LineStyle.Single;
/// <inheritdoc/>
public void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent)
{
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
{
if (_currentLine == null)
{
// Mouse pressed down
_currentLine = new (
mouseEvent.Position,
0,
Orientation.Vertical,
LineStyle,
area.CurrentAttribute
);
area.CurrentLayer.AddLine (_currentLine);
}
else
{
// Mouse dragged
Point start = _currentLine.Start;
Point end = mouseEvent.Position;
var orientation = Orientation.Vertical;
int length = end.Y - start.Y;
// if line is wider than it is tall switch to horizontal
if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y))
{
orientation = Orientation.Horizontal;
length = end.X - start.X;
}
if (length > 0)
{
length++;
}
else
{
length--;
}
_currentLine.Length = length;
_currentLine.Orientation = orientation;
area.CurrentLayer.ClearCache ();
area.SetNeedsDisplay ();
}
}
else
{
// Mouse released
if (_currentLine != null)
{
if (_currentLine.Length == 0)
{
_currentLine.Length = 1;
}
if (_currentLine.Style == LineStyle.None)
{
// Treat none as eraser
int idx = area.Layers.IndexOf (area.CurrentLayer);
area.Layers.Remove (area.CurrentLayer);
area.CurrentLayer = new (
area.CurrentLayer.Lines.Exclude (
_currentLine.Start,
_currentLine.Length,
_currentLine.Orientation
)
);
area.Layers.Insert (idx, area.CurrentLayer);
}
_currentLine = null;
area.ClearUndo ();
area.SetNeedsDisplay ();
}
}
}
}
[ScenarioMetadata ("Line Drawing", "Demonstrates LineCanvas.")]
[ScenarioCategory ("Controls")]
[ScenarioCategory ("Drawing")]
@@ -18,12 +112,14 @@ public class LineDrawing : Scenario
var tools = new ToolsView { Title = "Tools", X = Pos.Right (canvas) - 20, Y = 2 };
tools.ColorChanged += c => canvas.SetColor (c);
tools.SetStyle += b => canvas.LineStyle = b;
tools.ColorChanged += (s, e) => canvas.SetAttribute (e);
tools.SetStyle += b => canvas.CurrentTool = new DrawLineTool { LineStyle = b };
tools.AddLayer += () => canvas.AddLayer ();
win.Add (canvas);
win.Add (tools);
tools.CurrentColor = canvas.GetNormalColor ();
canvas.CurrentAttribute = tools.CurrentColor;
win.KeyDown += (s, e) => { e.Handled = canvas.OnKeyDown (e); };
@@ -32,206 +128,329 @@ public class LineDrawing : Scenario
Application.Shutdown ();
}
private class DrawingArea : View
public static bool PromptForColor (string title, Color current, out Color newColor)
{
private readonly List<LineCanvas> _layers = new ();
private readonly Stack<StraightLine> _undoHistory = new ();
private Color _currentColor = new (Color.White);
private LineCanvas _currentLayer;
private StraightLine _currentLine;
public DrawingArea () { AddLayer (); }
public LineStyle LineStyle { get; set; }
var accept = false;
public override void OnDrawContentComplete (Rectangle viewport)
var d = new Dialog
{
base.OnDrawContentComplete (viewport);
Title = title,
Height = 7
};
foreach (LineCanvas canvas in _layers)
var btnOk = new Button
{
X = Pos.Center () - 5,
Y = 4,
Text = "Ok",
Width = Dim.Auto (),
IsDefault = true
};
btnOk.Accept += (s, e) =>
{
accept = true;
e.Handled = true;
Application.RequestStop ();
};
var btnCancel = new Button
{
X = Pos.Center () + 5,
Y = 4,
Text = "Cancel",
Width = Dim.Auto ()
};
btnCancel.Accept += (s, e) =>
{
e.Handled = true;
Application.RequestStop ();
};
d.Add (btnOk);
d.Add (btnCancel);
/* Does not work
d.AddButton (btnOk);
d.AddButton (btnCancel);
*/
var cp = new ColorPicker
{
SelectedColor = current,
Width = Dim.Fill ()
};
d.Add (cp);
Application.Run (d);
d.Dispose ();
newColor = cp.SelectedColor;
return accept;
}
}
public class ToolsView : Window
{
private Button _addLayerBtn;
private readonly AttributeView _colors;
private RadioGroup _stylePicker;
public Attribute CurrentColor
{
get => _colors.Value;
set => _colors.Value = value;
}
public ToolsView ()
{
BorderStyle = LineStyle.Dotted;
Border.Thickness = new (1, 2, 1, 1);
Initialized += ToolsView_Initialized;
_colors = new ();
}
public event Action AddLayer;
public override void BeginInit ()
{
base.BeginInit ();
_colors.ValueChanged += (s, e) => ColorChanged?.Invoke (this, e);
_stylePicker = new()
{
X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray ()
};
_stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); };
_stylePicker.SelectedItem = 1;
_addLayerBtn = new() { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
_addLayerBtn.Accept += (s, a) => AddLayer?.Invoke ();
Add (_colors, _stylePicker, _addLayerBtn);
}
public event EventHandler<Attribute> ColorChanged;
public event Action<LineStyle> SetStyle;
private void ToolsView_Initialized (object sender, EventArgs e)
{
LayoutSubviews ();
Width = Math.Max (_colors.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal;
Height = _colors.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical;
SuperView.LayoutSubviews ();
}
}
public class DrawingArea : View
{
public readonly List<LineCanvas> Layers = new ();
private readonly Stack<StraightLine> _undoHistory = new ();
public Attribute CurrentAttribute { get; set; }
public LineCanvas CurrentLayer { get; set; }
public ITool CurrentTool { get; set; } = new DrawLineTool ();
public DrawingArea () { AddLayer (); }
public override void OnDrawContentComplete (Rectangle viewport)
{
base.OnDrawContentComplete (viewport);
foreach (LineCanvas canvas in Layers)
{
foreach (KeyValuePair<Point, Cell?> c in canvas.GetCellMap ())
{
foreach (KeyValuePair<Point, Cell?> c in canvas.GetCellMap ())
if (c.Value is { })
{
if (c.Value is { })
{
Driver.SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal);
Driver.SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal);
// TODO: #2616 - Support combining sequences that don't normalize
AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune);
}
// TODO: #2616 - Support combining sequences that don't normalize
AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune);
}
}
}
}
//// BUGBUG: Why is this not handled by a key binding???
public override bool OnKeyDown (Key e)
//// BUGBUG: Why is this not handled by a key binding???
public override bool OnKeyDown (Key e)
{
// BUGBUG: These should be implemented with key bindings
if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask))
{
// BUGBUG: These should be implemented with key bindings
if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask))
StraightLine pop = CurrentLayer.RemoveLastLine ();
if (pop != null)
{
StraightLine pop = _currentLayer.RemoveLastLine ();
_undoHistory.Push (pop);
SetNeedsDisplay ();
if (pop != null)
{
_undoHistory.Push (pop);
SetNeedsDisplay ();
return true;
}
return true;
}
if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask))
{
if (_undoHistory.Any ())
{
StraightLine pop = _undoHistory.Pop ();
_currentLayer.AddLine (pop);
SetNeedsDisplay ();
return true;
}
}
return false;
}
protected override bool OnMouseEvent (MouseEvent mouseEvent)
if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask))
{
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
if (_undoHistory.Any ())
{
if (_currentLine == null)
{
// Mouse pressed down
_currentLine = new StraightLine (
mouseEvent.Position,
0,
Orientation.Vertical,
LineStyle,
new Attribute (_currentColor, GetNormalColor ().Background)
);
StraightLine pop = _undoHistory.Pop ();
CurrentLayer.AddLine (pop);
SetNeedsDisplay ();
_currentLayer.AddLine (_currentLine);
}
else
{
// Mouse dragged
Point start = _currentLine.Start;
var end = mouseEvent.Position;
var orientation = Orientation.Vertical;
int length = end.Y - start.Y;
return true;
}
}
// if line is wider than it is tall switch to horizontal
if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y))
{
orientation = Orientation.Horizontal;
length = end.X - start.X;
}
return false;
}
if (length > 0)
{
length++;
}
else
{
length--;
}
protected override bool OnMouseEvent (MouseEvent mouseEvent)
{
CurrentTool.OnMouseEvent (this, mouseEvent);
_currentLine.Length = length;
_currentLine.Orientation = orientation;
_currentLayer.ClearCache ();
SetNeedsDisplay ();
}
return base.OnMouseEvent (mouseEvent);
}
internal void AddLayer ()
{
CurrentLayer = new ();
Layers.Add (CurrentLayer);
}
internal void SetAttribute (Attribute a) { CurrentAttribute = a; }
public void ClearUndo () { _undoHistory.Clear (); }
}
public class AttributeView : View
{
public event EventHandler<Attribute> ValueChanged;
private Attribute _value;
public Attribute Value
{
get => _value;
set
{
_value = value;
ValueChanged?.Invoke (this, value);
}
}
private static readonly HashSet<(int, int)> ForegroundPoints = new()
{
(0, 0), (1, 0), (2, 0),
(0, 1), (1, 1), (2, 1)
};
private static readonly HashSet<(int, int)> BackgroundPoints = new()
{
(3, 1),
(1, 2), (2, 2), (3, 2)
};
public AttributeView ()
{
Width = 4;
Height = 3;
}
/// <inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
Color fg = Value.Foreground;
Color bg = Value.Background;
bool isTransparentFg = fg == GetNormalColor ().Background;
bool isTransparentBg = bg == GetNormalColor ().Background;
Driver.SetAttribute (new (fg, isTransparentFg ? Color.Gray : fg));
// Square of foreground color
foreach ((int, int) point in ForegroundPoints)
{
// Make pattern like this when it is same color as background of control
/*▓▒
▒▓*/
Rune rune;
if (isTransparentFg)
{
rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
}
else
{
// Mouse released
if (_currentLine != null)
{
if (_currentLine.Length == 0)
{
_currentLine.Length = 1;
}
if (_currentLine.Style == LineStyle.None)
{
// Treat none as eraser
int idx = _layers.IndexOf (_currentLayer);
_layers.Remove (_currentLayer);
_currentLayer = new LineCanvas (
_currentLayer.Lines.Exclude (
_currentLine.Start,
_currentLine.Length,
_currentLine.Orientation
)
);
_layers.Insert (idx, _currentLayer);
}
_currentLine = null;
_undoHistory.Clear ();
SetNeedsDisplay ();
}
rune = (Rune)'█';
}
return base.OnMouseEvent (mouseEvent);
AddRune (point.Item1, point.Item2, rune);
}
internal void AddLayer ()
Driver.SetAttribute (new (bg, isTransparentBg ? Color.Gray : bg));
// Square of background color
foreach ((int, int) point in BackgroundPoints)
{
_currentLayer = new LineCanvas ();
_layers.Add (_currentLayer);
}
// Make pattern like this when it is same color as background of control
/*▓▒
▒▓*/
Rune rune;
internal void SetColor (Color c) { _currentColor = c; }
if (isTransparentBg)
{
rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
}
else
{
rune = (Rune)'█';
}
AddRune (point.Item1, point.Item2, rune);
}
}
private class ToolsView : Window
/// <inheritdoc/>
protected override bool OnMouseEvent (MouseEvent mouseEvent)
{
private Button _addLayerBtn;
private ColorPicker _colorPicker;
private RadioGroup _stylePicker;
public ToolsView ()
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
{
BorderStyle = LineStyle.Dotted;
Border.Thickness = new Thickness (1, 2, 1, 1);
Initialized += ToolsView_Initialized;
}
public event Action AddLayer;
public override void BeginInit ()
{
base.BeginInit ();
_colorPicker = new ColorPicker { X = 0, Y = 0, BoxHeight = 1, BoxWidth = 2 };
_colorPicker.ColorChanged += (s, a) => ColorChanged?.Invoke (a.Color);
_stylePicker = new RadioGroup
if (IsForegroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
{
X = 0, Y = Pos.Bottom (_colorPicker), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray ()
};
_stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); };
_stylePicker.SelectedItem = 1;
_addLayerBtn = new Button { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
_addLayerBtn.Accept += (s, a) => AddLayer?.Invoke ();
Add (_colorPicker, _stylePicker, _addLayerBtn);
ClickedInForeground ();
}
else if (IsBackgroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
{
ClickedInBackground ();
}
}
public event Action<Color> ColorChanged;
public event Action<LineStyle> SetStyle;
return base.OnMouseEvent (mouseEvent);
}
private void ToolsView_Initialized (object sender, EventArgs e)
private bool IsForegroundPoint (int x, int y) { return ForegroundPoints.Contains ((x, y)); }
private bool IsBackgroundPoint (int x, int y) { return BackgroundPoints.Contains ((x, y)); }
private void ClickedInBackground ()
{
if (LineDrawing.PromptForColor ("Background", Value.Background, out Color newColor))
{
LayoutSubviews ();
Value = new (Value.Foreground, newColor);
SetNeedsDisplay ();
}
}
Width = Math.Max (_colorPicker.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal;
Height = _colorPicker.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical;
SuperView.LayoutSubviews ();
private void ClickedInForeground ()
{
if (LineDrawing.PromptForColor ("Foreground", Value.Foreground, out Color newColor))
{
Value = new (newColor, Value.Background);
SetNeedsDisplay ();
}
}
}

View File

@@ -63,55 +63,25 @@ public class ProgressBarStyles : Scenario
#region ColorPicker
ColorName ChooseColor (string text, ColorName colorName)
{
var colorPicker = new ColorPicker { Title = text, SelectedColor = colorName };
var dialog = new Dialog { Title = text };
dialog.Initialized += (sender, args) =>
{
// TODO: Replace with Dim.Auto
dialog.X = _pbList.Frame.X;
dialog.Y = _pbList.Frame.Height;
};
dialog.LayoutComplete += (sender, args) =>
{
dialog.Viewport = Rectangle.Empty with
{
Width = colorPicker.Frame.Width,
Height = colorPicker.Frame.Height
};
Application.Top.LayoutSubviews ();
};
dialog.Add (colorPicker);
colorPicker.ColorChanged += (s, e) => { dialog.RequestStop (); };
Application.Run (dialog);
dialog.Dispose ();
ColorName retColor = colorPicker.SelectedColor;
colorPicker.Dispose ();
return retColor;
}
var fgColorPickerBtn = new Button
{
Text = "Foreground HotNormal Color",
X = Pos.Center (),
Y = Pos.Align (Alignment.Start),
Y = Pos.Align (Alignment.Start)
};
container.Add (fgColorPickerBtn);
fgColorPickerBtn.Accept += (s, e) =>
{
ColorName newColor = ChooseColor (
fgColorPickerBtn.Text,
editor.ViewToEdit.ColorScheme.HotNormal.Foreground
.GetClosestNamedColor ()
);
if (!LineDrawing.PromptForColor (
fgColorPickerBtn.Text,
editor.ViewToEdit.ColorScheme.HotNormal.Foreground,
out var newColor
))
{
return;
}
var cs = new ColorScheme (editor.ViewToEdit.ColorScheme)
{
@@ -134,11 +104,14 @@ public class ProgressBarStyles : Scenario
bgColorPickerBtn.Accept += (s, e) =>
{
ColorName newColor = ChooseColor (
fgColorPickerBtn.Text,
editor.ViewToEdit.ColorScheme.HotNormal.Background
.GetClosestNamedColor ()
);
if (!LineDrawing.PromptForColor (
fgColorPickerBtn.Text,
editor.ViewToEdit.ColorScheme.HotNormal.Background
, out var newColor))
{
return;
}
var cs = new ColorScheme (editor.ViewToEdit.ColorScheme)
{

View File

@@ -318,17 +318,17 @@ public class Shortcuts : Scenario
CanFocus = false
};
var bgColor = new ColorPicker ()
var bgColor = new ColorPicker16 ()
{
CanFocus = false,
BoxHeight = 1,
BoxWidth = 1,
CanFocus = false
};
bgColor.ColorChanged += (o, args) =>
{
Application.Top.ColorScheme = new ColorScheme (Application.Top.ColorScheme)
{
Normal = new Attribute (Application.Top.ColorScheme.Normal.Foreground, args.Color),
Normal = new Attribute (Application.Top.ColorScheme.Normal.Foreground, args.CurrentValue),
};
};
hShortcutBG.CommandView = bgColor;

View File

@@ -0,0 +1,79 @@
namespace Terminal.Gui.ViewsTests;
public class ColorPicker16Tests
{
[Fact]
public void Constructors ()
{
var colorPicker = new ColorPicker16 ();
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Assert.Equal (Point.Empty, colorPicker.Cursor);
Assert.True (colorPicker.CanFocus);
colorPicker.BeginInit ();
colorPicker.EndInit ();
colorPicker.LayoutSubviews ();
Assert.Equal (new (0, 0, 32, 4), colorPicker.Frame);
}
[Fact]
public void KeyBindings_Command ()
{
var colorPicker = new ColorPicker16 ();
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (ColorName.Blue, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorDown));
Assert.Equal (ColorName.BrightBlue, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (ColorName.DarkGray, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
}
[Fact]
[AutoInitShutdown]
public void MouseEvents ()
{
var colorPicker = new ColorPicker16 { X = 0, Y = 0, Height = 4, Width = 32 };
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
var top = new Toplevel ();
top.Add (colorPicker);
Application.Begin (top);
Assert.False (colorPicker.NewMouseEvent (new ()));
Assert.True (colorPicker.NewMouseEvent (new () { Position = new (4, 1), Flags = MouseFlags.Button1Clicked }));
Assert.Equal (ColorName.Blue, colorPicker.SelectedColor);
top.Dispose ();
}
[Fact]
public void SelectedColorAndCursor ()
{
var colorPicker = new ColorPicker16 ();
colorPicker.SelectedColor = ColorName.White;
Assert.Equal (7, colorPicker.Cursor.X);
Assert.Equal (1, colorPicker.Cursor.Y);
colorPicker.SelectedColor = Color.Black;
Assert.Equal (0, colorPicker.Cursor.X);
Assert.Equal (0, colorPicker.Cursor.Y);
colorPicker.Cursor = new (7, 1);
Assert.Equal (ColorName.White, colorPicker.SelectedColor);
colorPicker.Cursor = Point.Empty;
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
}
}

View File

@@ -1,79 +1,766 @@
namespace Terminal.Gui.ViewsTests;
using Xunit.Abstractions;
using Color = Terminal.Gui.Color;
namespace Terminal.Gui.ViewsTests;
public class ColorPickerTests
{
[Fact]
public void Constructors ()
[SetupFakeDriver]
public void ColorPicker_Construct_DefaultValue ()
{
var colorPicker = new ColorPicker ();
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Assert.Equal (Point.Empty, colorPicker.Cursor);
Assert.True (colorPicker.CanFocus);
var cp = GetColorPicker (ColorModel.HSV, false);
colorPicker.BeginInit ();
colorPicker.EndInit ();
colorPicker.LayoutSubviews ();
Assert.Equal (new (0, 0, 32, 4), colorPicker.Frame);
// Should be only a single text field (Hex) because ShowTextFields is false
Assert.Single (cp.Subviews.OfType<TextField> ());
cp.Draw ();
// All bars should be at 0 with the triangle at 0 (+2 because of "H:", "S:" etc)
var h = GetColorBar (cp, ColorPickerPart.Bar1);
Assert.Equal ("H:", h.Text);
Assert.Equal (2, h.TrianglePosition);
Assert.IsType<HueBar> (h);
var s = GetColorBar (cp, ColorPickerPart.Bar2);
Assert.Equal ("S:", s.Text);
Assert.Equal (2, s.TrianglePosition);
Assert.IsType<SaturationBar> (s);
var v = GetColorBar (cp, ColorPickerPart.Bar3);
Assert.Equal ("V:", v.Text);
Assert.Equal (2, v.TrianglePosition);
Assert.IsType<ValueBar> (v);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("#000000", hex.Text);
}
[Fact]
public void KeyBindings_Command ()
[SetupFakeDriver]
public void ColorPicker_RGB_KeyboardNavigation ()
{
var colorPicker = new ColorPicker ();
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
var cp = GetColorPicker (ColorModel.RGB, false);
cp.Draw ();
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorRight));
Assert.Equal (ColorName.Blue, colorPicker.SelectedColor);
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorDown));
Assert.Equal (ColorName.BrightBlue, colorPicker.SelectedColor);
Assert.Equal ("R:", r.Text);
Assert.Equal (2, r.TrianglePosition);
Assert.IsType<RBar> (r);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.IsType<GBar> (g);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.IsType<BBar> (b);
Assert.Equal ("#000000", hex.Text);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (ColorName.DarkGray, colorPicker.SelectedColor);
Assert.IsAssignableFrom<IColorBar> (cp.Focused);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
cp.Draw ();
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
Application.OnKeyDown (Key.CursorRight);
Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp));
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
cp.Draw ();
Assert.Equal (3, r.TrianglePosition);
Assert.Equal ("#0F0000", hex.Text);
Application.OnKeyDown (Key.CursorRight);
cp.Draw ();
Assert.Equal (4, r.TrianglePosition);
Assert.Equal ("#1E0000", hex.Text);
// Use cursor to move the triangle all the way to the right
for (int i = 0; i < 1000; i++)
{
Application.OnKeyDown (Key.CursorRight);
}
cp.Draw ();
// 20 width and TrianglePosition is 0 indexed
// Meaning we are asserting that triangle is at end
Assert.Equal (19, r.TrianglePosition);
Assert.Equal ("#FF0000", hex.Text);
Application.Current.Dispose ();
}
[Fact]
[AutoInitShutdown]
public void MouseEvents ()
[SetupFakeDriver]
public void ColorPicker_RGB_MouseNavigation ()
{
var colorPicker = new ColorPicker { X = 0, Y = 0, Height = 4, Width = 32 };
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
var top = new Toplevel ();
top.Add (colorPicker);
Application.Begin (top);
var cp = GetColorPicker (ColorModel.RGB,false);
Assert.False (colorPicker.NewMouseEvent (new ()));
cp.Draw ();
Assert.True (colorPicker.NewMouseEvent (new() { Position = new (4, 1), Flags = MouseFlags.Button1Clicked }));
Assert.Equal (ColorName.Blue, colorPicker.SelectedColor);
top.Dispose ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("R:", r.Text);
Assert.Equal (2, r.TrianglePosition);
Assert.IsType<RBar> (r);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.IsType<GBar> (g);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.IsType<BBar> (b);
Assert.Equal ("#000000", hex.Text);
Assert.IsAssignableFrom<IColorBar> (cp.Focused);
cp.Focused.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (3, 0)
});
cp.Draw ();
Assert.Equal (3, r.TrianglePosition);
Assert.Equal ("#0F0000", hex.Text);
cp.Focused.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (4, 0)
});
cp.Draw ();
Assert.Equal (4, r.TrianglePosition);
Assert.Equal ("#1E0000", hex.Text);
Application.Current?.Dispose ();
}
public static IEnumerable<object []> ColorPickerTestData ()
{
yield return new object []
{
new Color(255, 0),
"R:", 19, "G:", 2, "B:", 2, "#FF0000"
};
yield return new object []
{
new Color(0, 255),
"R:", 2, "G:", 19, "B:", 2, "#00FF00"
};
yield return new object []
{
new Color(0, 0, 255),
"R:", 2, "G:", 2, "B:", 19, "#0000FF"
};
yield return new object []
{
new Color(125, 125, 125),
"R:", 11, "G:", 11, "B:", 11, "#7D7D7D"
};
}
[Theory]
[SetupFakeDriver]
[MemberData (nameof (ColorPickerTestData))]
public void ColorPicker_RGB_NoText (Color c, string expectedR, int expectedRTriangle, string expectedG, int expectedGTriangle, string expectedB, int expectedBTriangle, string expectedHex)
{
var cp = GetColorPicker (ColorModel.RGB, false);
cp.SelectedColor = c;
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal (expectedR, r.Text);
Assert.Equal (expectedRTriangle, r.TrianglePosition);
Assert.Equal (expectedG, g.Text);
Assert.Equal (expectedGTriangle, g.TrianglePosition);
Assert.Equal (expectedB, b.Text);
Assert.Equal (expectedBTriangle, b.TrianglePosition);
Assert.Equal (expectedHex, hex.Text);
Application.Current.Dispose ();
}
public static IEnumerable<object []> ColorPickerTestData_WithTextFields ()
{
yield return new object []
{
new Color(255, 0),
"R:", 15, 255, "G:", 2, 0, "B:", 2, 0, "#FF0000"
};
yield return new object []
{
new Color(0, 255),
"R:", 2, 0, "G:", 15, 255, "B:", 2, 0, "#00FF00"
};
yield return new object []
{
new Color(0, 0, 255),
"R:", 2, 0, "G:", 2, 0, "B:", 15, 255, "#0000FF"
};
yield return new object []
{
new Color(125, 125, 125),
"R:", 9, 125, "G:", 9, 125, "B:", 9, 125, "#7D7D7D"
};
}
[Theory]
[SetupFakeDriver]
[MemberData (nameof (ColorPickerTestData_WithTextFields))]
public void ColorPicker_RGB_NoText_WithTextFields (Color c, string expectedR, int expectedRTriangle, int expectedRValue, string expectedG, int expectedGTriangle, int expectedGValue, string expectedB, int expectedBTriangle, int expectedBValue, string expectedHex)
{
var cp = GetColorPicker (ColorModel.RGB, true);
cp.SelectedColor = c;
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
var rTextField = GetTextField (cp, ColorPickerPart.Bar1);
var gTextField = GetTextField (cp, ColorPickerPart.Bar2);
var bTextField = GetTextField (cp, ColorPickerPart.Bar3);
Assert.Equal (expectedR, r.Text);
Assert.Equal (expectedRTriangle, r.TrianglePosition);
Assert.Equal (expectedRValue.ToString (), rTextField.Text);
Assert.Equal (expectedG, g.Text);
Assert.Equal (expectedGTriangle, g.TrianglePosition);
Assert.Equal (expectedGValue.ToString (), gTextField.Text);
Assert.Equal (expectedB, b.Text);
Assert.Equal (expectedBTriangle, b.TrianglePosition);
Assert.Equal (expectedBValue.ToString (), bTextField.Text);
Assert.Equal (expectedHex, hex.Text);
Application.Current?.Dispose ();
}
[Fact]
public void SelectedColorAndCursor ()
[SetupFakeDriver]
public void ColorPicker_ClickingAtEndOfBar_SetsMaxValue ()
{
var colorPicker = new ColorPicker ();
colorPicker.SelectedColor = ColorName.White;
Assert.Equal (7, colorPicker.Cursor.X);
Assert.Equal (1, colorPicker.Cursor.Y);
var cp = GetColorPicker (ColorModel.RGB, false);
colorPicker.SelectedColor = Color.Black;
Assert.Equal (0, colorPicker.Cursor.X);
Assert.Equal (0, colorPicker.Cursor.Y);
cp.Draw ();
colorPicker.Cursor = new (7, 1);
Assert.Equal (ColorName.White, colorPicker.SelectedColor);
// Click at the end of the Red bar
cp.Focused.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (19, 0) // Assuming 0-based indexing
});
colorPicker.Cursor = Point.Empty;
Assert.Equal (ColorName.Black, colorPicker.SelectedColor);
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("R:", r.Text);
Assert.Equal (19, r.TrianglePosition);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("#FF0000", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_ClickingBeyondBar_ChangesToMaxValue ()
{
var cp = GetColorPicker (ColorModel.RGB, false);
cp.Draw ();
// Click beyond the bar
cp.Focused.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (21, 0) // Beyond the bar
});
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("R:", r.Text);
Assert.Equal (19, r.TrianglePosition);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("#FF0000", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_ChangeValueOnUI_UpdatesAllUIElements ()
{
var cp = GetColorPicker (ColorModel.RGB, true);
View otherView = new View () { CanFocus = true };
Application.Current?.Add (otherView);
cp.Draw ();
// Change value using text field
TextField rBarTextField = cp.Subviews.OfType<TextField> ().First (tf => tf.Text == "0");
rBarTextField.Text = "128";
//rBarTextField.OnLeave (cp); // OnLeave should be protected virtual. Don't call it.
otherView.SetFocus (); // Remove focus from the color picker
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
var rTextField = GetTextField (cp, ColorPickerPart.Bar1);
var gTextField = GetTextField (cp, ColorPickerPart.Bar2);
var bTextField = GetTextField (cp, ColorPickerPart.Bar3);
Assert.Equal ("R:", r.Text);
Assert.Equal (9, r.TrianglePosition);
Assert.Equal ("128", rTextField.Text);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("0", gTextField.Text);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("0", bTextField.Text);
Assert.Equal ("#800000", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_InvalidHexInput_DoesNotChangeColor ()
{
var cp = GetColorPicker (ColorModel.RGB, true);
cp.Draw ();
// Enter invalid hex value
TextField hexField = cp.Subviews.OfType<TextField> ().First (tf => tf.Text == "#000000");
hexField.Text = "#ZZZZZZ";
hexField.OnLeave (cp);
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("R:", r.Text);
Assert.Equal (2, r.TrianglePosition);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("#000000", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_ClickingDifferentBars_ChangesFocus ()
{
var cp = GetColorPicker (ColorModel.RGB, false);
cp.Draw ();
// Click on Green bar
cp.Subviews.OfType<GBar> ()
.Single ()
.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (0, 1)
});
cp.Draw ();
Assert.IsAssignableFrom<GBar> (cp.Focused);
// Click on Blue bar
cp.Subviews.OfType<BBar> ()
.Single ()
.OnMouseEvent (
new ()
{
Flags = MouseFlags.Button1Pressed,
Position = new (0, 2)
});
cp.Draw ();
Assert.IsAssignableFrom<BBar> (cp.Focused);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_SwitchingColorModels_ResetsBars ()
{
var cp = GetColorPicker (ColorModel.RGB, false);
cp.SelectedColor = new (255, 0);
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.Equal ("R:", r.Text);
Assert.Equal (19, r.TrianglePosition);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("#FF0000", hex.Text);
// Switch to HSV
cp.Style.ColorModel = ColorModel.HSV;
cp.ApplyStyleChanges ();
cp.Draw ();
var h = GetColorBar (cp, ColorPickerPart.Bar1);
var s = GetColorBar (cp, ColorPickerPart.Bar2);
var v = GetColorBar (cp, ColorPickerPart.Bar3);
Assert.Equal ("H:", h.Text);
Assert.Equal (2, h.TrianglePosition);
Assert.Equal ("S:", s.Text);
Assert.Equal (19, s.TrianglePosition);
Assert.Equal ("V:", v.Text);
Assert.Equal (19, v.TrianglePosition);
Assert.Equal ("#FF0000", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_SyncBetweenTextFieldAndBars ()
{
var cp = GetColorPicker (ColorModel.RGB, true);
cp.Draw ();
// Change value using the bar
RBar rBar = cp.Subviews.OfType<RBar> ().First ();
rBar.Value = 128;
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
var rTextField = GetTextField (cp, ColorPickerPart.Bar1);
var gTextField = GetTextField (cp, ColorPickerPart.Bar2);
var bTextField = GetTextField (cp, ColorPickerPart.Bar3);
Assert.Equal ("R:", r.Text);
Assert.Equal (9, r.TrianglePosition);
Assert.Equal ("128", rTextField.Text);
Assert.Equal ("G:", g.Text);
Assert.Equal (2, g.TrianglePosition);
Assert.Equal ("0", gTextField.Text);
Assert.Equal ("B:", b.Text);
Assert.Equal (2, b.TrianglePosition);
Assert.Equal ("0", bTextField.Text);
Assert.Equal ("#800000", hex.Text);
Application.Current?.Dispose ();
}
enum ColorPickerPart
{
Bar1 = 0,
Bar2 = 1,
Bar3 = 2,
ColorName = 3,
Hex = 4,
}
private TextField GetTextField (ColorPicker cp, ColorPickerPart toGet)
{
var hasBarValueTextFields = cp.Style.ShowTextFields;
var hasColorNameTextField = cp.Style.ShowColorName;
switch (toGet)
{
case ColorPickerPart.Bar1:
case ColorPickerPart.Bar2:
case ColorPickerPart.Bar3:
if (!hasBarValueTextFields)
{
throw new NotSupportedException ("Corresponding Style option is not enabled");
}
return cp.Subviews.OfType<TextField> ().ElementAt ((int)toGet);
case ColorPickerPart.ColorName:
if (!hasColorNameTextField)
{
throw new NotSupportedException ("Corresponding Style option is not enabled");
}
return cp.Subviews.OfType<TextField> ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet -3);
case ColorPickerPart.Hex:
int offset = hasBarValueTextFields ? 0 : 3;
offset += hasColorNameTextField ? 0 : 1;
return cp.Subviews.OfType<TextField> ().ElementAt ((int)toGet - offset);
default:
throw new ArgumentOutOfRangeException (nameof (toGet), toGet, null);
}
}
private ColorBar GetColorBar (ColorPicker cp, ColorPickerPart toGet)
{
if (toGet <= ColorPickerPart.Bar3)
{
return cp.Subviews.OfType<ColorBar> ().ElementAt ((int)toGet);
}
throw new NotSupportedException ("ColorPickerPart must be a bar");
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_ChangedEvent_Fires ()
{
Color newColor = default;
var count = 0;
var cp = new ColorPicker ();
cp.ColorChanged += (s, e) =>
{
count++;
newColor = e.CurrentValue;
Assert.Equal (cp.SelectedColor, e.CurrentValue);
};
cp.SelectedColor = new (1, 2, 3);
Assert.Equal (1, count);
Assert.Equal (new (1, 2, 3), newColor);
cp.SelectedColor = new (2, 3, 4);
Assert.Equal (2, count);
Assert.Equal (new (2, 3, 4), newColor);
// Set to same value
cp.SelectedColor = new (2, 3, 4);
// Should have no effect
Assert.Equal (2, count);
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_DisposesOldViews_OnModelChange ()
{
var cp = GetColorPicker (ColorModel.HSL,true);
var b1 = GetColorBar (cp, ColorPickerPart.Bar1);
var b2 = GetColorBar (cp, ColorPickerPart.Bar2);
var b3 = GetColorBar (cp, ColorPickerPart.Bar3);
var tf1 = GetTextField (cp, ColorPickerPart.Bar1);
var tf2 = GetTextField (cp, ColorPickerPart.Bar2);
var tf3 = GetTextField (cp, ColorPickerPart.Bar3);
var hex = GetTextField (cp, ColorPickerPart.Hex);
Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.False (b.WasDisposed));
cp.Style.ColorModel = ColorModel.RGB;
cp.ApplyStyleChanges ();
var b1After = GetColorBar (cp, ColorPickerPart.Bar1);
var b2After = GetColorBar (cp, ColorPickerPart.Bar2);
var b3After = GetColorBar (cp, ColorPickerPart.Bar3);
var tf1After = GetTextField (cp, ColorPickerPart.Bar1);
var tf2After = GetTextField (cp, ColorPickerPart.Bar2);
var tf3After = GetTextField (cp, ColorPickerPart.Bar3);
var hexAfter = GetTextField (cp, ColorPickerPart.Hex);
// Old bars should be disposed
Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.True (b.WasDisposed));
Assert.NotSame (hex,hexAfter);
Assert.NotSame (b1,b1After);
Assert.NotSame (b2, b2After);
Assert.NotSame (b3, b3After);
Assert.NotSame (tf1, tf1After);
Assert.NotSame (tf2, tf2After);
Assert.NotSame (tf3, tf3After);
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_TabCompleteColorName()
{
var cp = GetColorPicker (ColorModel.RGB, true,true);
cp.Draw ();
var r = GetColorBar (cp, ColorPickerPart.Bar1);
var g = GetColorBar (cp, ColorPickerPart.Bar2);
var b = GetColorBar (cp, ColorPickerPart.Bar3);
var name = GetTextField (cp, ColorPickerPart.ColorName);
var hex = GetTextField (cp, ColorPickerPart.Hex);
name.FocusFirst (TabBehavior.TabStop);
Assert.True (name.HasFocus);
Assert.Same (name, cp.Focused);
name.Text = "";
Assert.Empty (name.Text);
Application.OnKeyDown (Key.A);
Application.OnKeyDown (Key.Q);
Assert.Equal ("aq",name.Text);
// Auto complete the color name
Application.OnKeyDown (Key.Tab);
Assert.Equal ("Aquamarine", name.Text);
// Tab out of the text field
Application.OnKeyDown (Key.Tab);
Assert.False (name.HasFocus);
Assert.NotSame (name, cp.Focused);
Assert.Equal ("#7FFFD4", hex.Text);
Application.Current?.Dispose ();
}
[Fact]
[SetupFakeDriver]
public void ColorPicker_EnterHexFor_ColorName ()
{
var cp = GetColorPicker (ColorModel.RGB, true, true);
cp.Draw ();
var name = GetTextField (cp, ColorPickerPart.ColorName);
var hex = GetTextField (cp, ColorPickerPart.Hex);
hex.FocusFirst (TabBehavior.TabStop);
Assert.True (hex.HasFocus);
Assert.Same (hex, cp.Focused);
hex.Text = "";
name.Text = "";
Assert.Empty (hex.Text);
Assert.Empty (name.Text);
Application.OnKeyDown ('#');
Assert.Empty (name.Text);
//7FFFD4
Assert.Equal ("#", hex.Text);
Application.OnKeyDown ('7');
Application.OnKeyDown ('F');
Application.OnKeyDown ('F');
Application.OnKeyDown ('F');
Application.OnKeyDown ('D');
Assert.Empty (name.Text);
Application.OnKeyDown ('4');
// Tab out of the text field
Application.OnKeyDown (Key.Tab);
Assert.False (hex.HasFocus);
Assert.NotSame (hex, cp.Focused);
// Color name should be recognised as a known string and populated
Assert.Equal ("#7FFFD4", hex.Text);
Assert.Equal("Aquamarine", name.Text);
Application.Current?.Dispose ();
}
[Fact]
public void TestColorNames ()
{
var colors = new W3CColors ();
Assert.Contains ("Aquamarine", colors.GetColorNames ());
Assert.DoesNotContain ("Save as",colors.GetColorNames ());
}
private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, bool showName = false)
{
var cp = new ColorPicker { Width = 20, SelectedColor = new (0, 0) };
cp.Style.ColorModel = colorModel;
cp.Style.ShowTextFields = showTextFields;
cp.Style.ShowColorName = showName;
cp.ApplyStyleChanges ();
Application.Current = new Toplevel () { Width = 20 ,Height = 5};
Application.Current.Add (cp);
Application.Current.FocusFirst (null);
Application.Current.LayoutSubviews ();
Application.Current.FocusFirst (null);
return cp;
}
}