#nullable enable namespace Terminal.Gui; /// /// A bar representing a single component of a e.g. /// the Red portion of a . /// internal abstract class ColorBar : View, IColorBar { /// /// Creates a new instance of the class. /// 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.LeftStart, _ => 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.LeftStart); KeyBindings.Add (Key.End, Command.RightEnd); } /// /// X coordinate that the bar starts at excluding any label. /// private int _barStartsAt; /// /// 0-1 for how much of the color element is present currently (HSL) /// private int _value; /// /// The amount of represented by each cell width on the bar /// Can be less than 1 e.g. if Saturation (0-100) and width > 100 /// private double _cellValue = 1d; /// /// Last known width of the bar as passed to . /// private int _barWidth; /// /// The currently selected amount of the color component stored by this class e.g. /// the amount of Hue in a . /// public int Value { get => _value; set { int clampedValue = Math.Clamp (value, 0, MaxValue); if (_value != clampedValue) { _value = clampedValue; OnValueChanged (); } } } /// void IColorBar.SetValueWithoutRaisingEvent (int v) { _value = v; SetNeedsDraw (); } /// protected override bool OnDrawingContent (Rectangle viewport) { var xOffset = 0; if (!string.IsNullOrWhiteSpace (Text)) { Move (0, 0); 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); return true; } /// /// Event fired when is changed to a new value /// public event EventHandler>? ValueChanged; /// protected override bool OnMouseEvent (MouseEventArgs 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; SetFocus (); } return mouseEvent.Handled; } /// /// When overriden in a derived class, returns the to /// render at proportion of the full bars width. /// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100. /// /// /// protected abstract Color GetColor (double fraction); /// /// 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. /// protected abstract int MaxValue { get; } /// /// The last drawn location in View's viewport where the Triangle appeared. /// Used exclusively for tests. /// 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 SetAttribute (new (triangleColor, color)); AddRune (x + xOffset, yOffset, new ('▲')); // Record for tests TrianglePosition = x + xOffset; } else { SetAttribute (new (color, color)); AddRune (x + xOffset, yOffset, new ('█')); } } } private void OnValueChanged () { ValueChanged?.Invoke (this, new (in _value)); SetNeedsDraw (); } private bool? SetMax () { Value = MaxValue; return true; } private bool? SetZero () { Value = 0; return true; } }