ColorBar.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// A bar representing a single component of a <see cref="Color"/> e.g.
  5. /// the Red portion of a <see cref="ColorModel.RGB"/>.
  6. /// </summary>
  7. internal abstract class ColorBar : View, IColorBar
  8. {
  9. /// <summary>
  10. /// Creates a new instance of the <see cref="ColorBar"/> class.
  11. /// </summary>
  12. protected ColorBar ()
  13. {
  14. Height = 1;
  15. Width = Dim.Fill ();
  16. CanFocus = true;
  17. AddCommand (Command.Left, _ => Adjust (-1));
  18. AddCommand (Command.Right, _ => Adjust (1));
  19. AddCommand (Command.LeftExtend, _ => Adjust (-MaxValue / 20));
  20. AddCommand (Command.RightExtend, _ => Adjust (MaxValue / 20));
  21. AddCommand (Command.LeftStart, _ => SetZero ());
  22. AddCommand (Command.RightEnd, _ => SetMax ());
  23. KeyBindings.Add (Key.CursorLeft, Command.Left);
  24. KeyBindings.Add (Key.CursorRight, Command.Right);
  25. KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend);
  26. KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend);
  27. KeyBindings.Add (Key.Home, Command.LeftStart);
  28. KeyBindings.Add (Key.End, Command.RightEnd);
  29. }
  30. /// <summary>
  31. /// X coordinate that the bar starts at excluding any label.
  32. /// </summary>
  33. private int _barStartsAt;
  34. /// <summary>
  35. /// 0-1 for how much of the color element is present currently (HSL)
  36. /// </summary>
  37. private int _value;
  38. /// <summary>
  39. /// The amount of <see cref="Value"/> represented by each cell width on the bar
  40. /// Can be less than 1 e.g. if Saturation (0-100) and width > 100
  41. /// </summary>
  42. private double _cellValue = 1d;
  43. /// <summary>
  44. /// Last known width of the bar as passed to <see cref="DrawBar"/>.
  45. /// </summary>
  46. private int _barWidth;
  47. /// <summary>
  48. /// The currently selected amount of the color component stored by this class e.g.
  49. /// the amount of Hue in a <see cref="ColorModel.HSL"/>.
  50. /// </summary>
  51. public int Value
  52. {
  53. get => _value;
  54. set
  55. {
  56. int clampedValue = Math.Clamp (value, 0, MaxValue);
  57. if (_value != clampedValue)
  58. {
  59. _value = clampedValue;
  60. OnValueChanged ();
  61. }
  62. }
  63. }
  64. /// <inheritdoc/>
  65. void IColorBar.SetValueWithoutRaisingEvent (int v)
  66. {
  67. _value = v;
  68. SetNeedsDraw ();
  69. }
  70. /// <inheritdoc/>
  71. protected override bool OnDrawingContent (Rectangle viewport)
  72. {
  73. var xOffset = 0;
  74. if (!string.IsNullOrWhiteSpace (Text))
  75. {
  76. Move (0, 0);
  77. SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
  78. Driver?.AddStr (Text);
  79. // TODO: is there a better method than this? this is what it is in TableView
  80. xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ());
  81. }
  82. _barWidth = viewport.Width - xOffset;
  83. _barStartsAt = xOffset;
  84. DrawBar (xOffset, 0, _barWidth);
  85. return true;
  86. }
  87. /// <summary>
  88. /// Event fired when <see cref="Value"/> is changed to a new value
  89. /// </summary>
  90. public event EventHandler<EventArgs<int>>? ValueChanged;
  91. /// <inheritdoc/>
  92. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  93. {
  94. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  95. {
  96. if (mouseEvent.Position.X >= _barStartsAt)
  97. {
  98. double v = MaxValue * ((double)mouseEvent.Position.X - _barStartsAt) / (_barWidth - 1);
  99. Value = Math.Clamp ((int)v, 0, MaxValue);
  100. }
  101. mouseEvent.Handled = true;
  102. SetFocus ();
  103. }
  104. return mouseEvent.Handled;
  105. }
  106. /// <summary>
  107. /// When overriden in a derived class, returns the <see cref="Color"/> to
  108. /// render at <paramref name="fraction"/> proportion of the full bars width.
  109. /// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100.
  110. /// </summary>
  111. /// <param name="fraction"></param>
  112. /// <returns></returns>
  113. protected abstract Color GetColor (double fraction);
  114. /// <summary>
  115. /// The maximum value allowed for this component e.g. Saturation allows up to 100 as it
  116. /// is a percentage while Hue allows up to 360 as it is measured in degrees.
  117. /// </summary>
  118. protected abstract int MaxValue { get; }
  119. /// <summary>
  120. /// The last drawn location in View's viewport where the Triangle appeared.
  121. /// Used exclusively for tests.
  122. /// </summary>
  123. internal int TrianglePosition { get; private set; }
  124. private bool? Adjust (int delta)
  125. {
  126. var change = (int)(delta * _cellValue);
  127. // Ensure that the change is at least 1 or -1 if delta is non-zero
  128. if (change == 0 && delta != 0)
  129. {
  130. change = delta > 0 ? 1 : -1;
  131. }
  132. Value += change;
  133. return true;
  134. }
  135. private void DrawBar (int xOffset, int yOffset, int width)
  136. {
  137. // Each 1 unit of X in the bar corresponds to this much of Value
  138. _cellValue = (double)MaxValue / (width - 1);
  139. for (var x = 0; x < width; x++)
  140. {
  141. double fraction = (double)x / (width - 1);
  142. Color color = GetColor (fraction);
  143. // Adjusted isSelectedCell calculation
  144. double cellBottomThreshold = (x - 1) * _cellValue;
  145. double cellTopThreshold = x * _cellValue;
  146. if (x == width - 1)
  147. {
  148. cellTopThreshold = MaxValue;
  149. }
  150. bool isSelectedCell = Value > cellBottomThreshold && Value <= cellTopThreshold;
  151. // Check the brightness of the background color
  152. double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
  153. Color triangleColor = Color.Black;
  154. if (brightness < 0.15) // Threshold to determine if the color is too close to black
  155. {
  156. triangleColor = Color.DarkGray;
  157. }
  158. if (isSelectedCell)
  159. {
  160. // Draw the triangle at the closest position
  161. SetAttribute (new (triangleColor, color));
  162. AddRune (x + xOffset, yOffset, new ('▲'));
  163. // Record for tests
  164. TrianglePosition = x + xOffset;
  165. }
  166. else
  167. {
  168. SetAttribute (new (color, color));
  169. AddRune (x + xOffset, yOffset, new ('█'));
  170. }
  171. }
  172. }
  173. private void OnValueChanged ()
  174. {
  175. ValueChanged?.Invoke (this, new (in _value));
  176. SetNeedsDraw ();
  177. }
  178. private bool? SetMax ()
  179. {
  180. Value = MaxValue;
  181. return true;
  182. }
  183. private bool? SetZero ()
  184. {
  185. Value = 0;
  186. return true;
  187. }
  188. }