ColorBar.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. SetNeedsDisplay ();
  69. }
  70. /// <inheritdoc/>
  71. public override void OnDrawContent (Rectangle viewport)
  72. {
  73. base.OnDrawContent (viewport);
  74. var xOffset = 0;
  75. if (!string.IsNullOrWhiteSpace (Text))
  76. {
  77. Move (0, 0);
  78. Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
  79. Driver.AddStr (Text);
  80. // TODO: is there a better method than this? this is what it is in TableView
  81. xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ());
  82. }
  83. _barWidth = viewport.Width - xOffset;
  84. _barStartsAt = xOffset;
  85. DrawBar (xOffset, 0, _barWidth);
  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 internal override bool OnMouseEvent (MouseEvent 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. return true;
  104. }
  105. return base.OnMouseEvent (mouseEvent);
  106. }
  107. /// <summary>
  108. /// When overriden in a derived class, returns the <see cref="Color"/> to
  109. /// render at <paramref name="fraction"/> proportion of the full bars width.
  110. /// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100.
  111. /// </summary>
  112. /// <param name="fraction"></param>
  113. /// <returns></returns>
  114. protected abstract Color GetColor (double fraction);
  115. /// <summary>
  116. /// The maximum value allowed for this component e.g. Saturation allows up to 100 as it
  117. /// is a percentage while Hue allows up to 360 as it is measured in degrees.
  118. /// </summary>
  119. protected abstract int MaxValue { get; }
  120. /// <summary>
  121. /// The last drawn location in View's viewport where the Triangle appeared.
  122. /// Used exclusively for tests.
  123. /// </summary>
  124. internal int TrianglePosition { get; private set; }
  125. private bool? Adjust (int delta)
  126. {
  127. var change = (int)(delta * _cellValue);
  128. // Ensure that the change is at least 1 or -1 if delta is non-zero
  129. if (change == 0 && delta != 0)
  130. {
  131. change = delta > 0 ? 1 : -1;
  132. }
  133. Value += change;
  134. return true;
  135. }
  136. private void DrawBar (int xOffset, int yOffset, int width)
  137. {
  138. // Each 1 unit of X in the bar corresponds to this much of Value
  139. _cellValue = (double)MaxValue / (width - 1);
  140. for (var x = 0; x < width; x++)
  141. {
  142. double fraction = (double)x / (width - 1);
  143. Color color = GetColor (fraction);
  144. // Adjusted isSelectedCell calculation
  145. double cellBottomThreshold = (x - 1) * _cellValue;
  146. double cellTopThreshold = x * _cellValue;
  147. if (x == width - 1)
  148. {
  149. cellTopThreshold = MaxValue;
  150. }
  151. bool isSelectedCell = Value > cellBottomThreshold && Value <= cellTopThreshold;
  152. // Check the brightness of the background color
  153. double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
  154. Color triangleColor = Color.Black;
  155. if (brightness < 0.15) // Threshold to determine if the color is too close to black
  156. {
  157. triangleColor = Color.DarkGray;
  158. }
  159. if (isSelectedCell)
  160. {
  161. // Draw the triangle at the closest position
  162. Application.Driver?.SetAttribute (new (triangleColor, color));
  163. AddRune (x + xOffset, yOffset, new ('▲'));
  164. // Record for tests
  165. TrianglePosition = x + xOffset;
  166. }
  167. else
  168. {
  169. Application.Driver?.SetAttribute (new (color, color));
  170. AddRune (x + xOffset, yOffset, new ('█'));
  171. }
  172. }
  173. }
  174. private void OnValueChanged ()
  175. {
  176. ValueChanged?.Invoke (this, new (in _value));
  177. SetNeedsDisplay ();
  178. }
  179. private bool? SetMax ()
  180. {
  181. Value = MaxValue;
  182. return true;
  183. }
  184. private bool? SetZero ()
  185. {
  186. Value = 0;
  187. return true;
  188. }
  189. }