ColorBar.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. using ColorHelper;
  2. namespace Terminal.Gui.Views;
  3. /// <summary>
  4. /// A bar representing a single component of a <see cref="Color"/> e.g.
  5. /// the Red portion of a <see cref="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 = Dim.Auto (minimumContentDim: 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="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 void OnSubViewsLaidOut (LayoutEventArgs args)
  72. {
  73. base.OnSubViewsLaidOut (args);
  74. var xOffset = 0;
  75. if (!string.IsNullOrWhiteSpace (Text))
  76. {
  77. // TODO: is there a better method than this? this is what it is in TableView
  78. xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ());
  79. }
  80. _barWidth = Viewport.Width - xOffset;
  81. _barStartsAt = xOffset;
  82. // Each 1 unit of X in the bar corresponds to this much of Value
  83. _cellValue = (double)MaxValue / (_barWidth - 1);
  84. }
  85. /// <inheritdoc/>
  86. protected override bool OnDrawingContent (DrawContext? context)
  87. {
  88. if (!string.IsNullOrWhiteSpace (Text))
  89. {
  90. Move (0, 0);
  91. SetAttribute (HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal));
  92. AddStr (Text);
  93. }
  94. DrawBar (_barStartsAt, 0, _barWidth);
  95. return true;
  96. }
  97. /// <summary>
  98. /// Event fired when <see cref="Value"/> is changed to a new value
  99. /// </summary>
  100. public event EventHandler<EventArgs<int>>? ValueChanged;
  101. /// <inheritdoc/>
  102. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  103. {
  104. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  105. {
  106. if (mouseEvent.Position.X >= _barStartsAt)
  107. {
  108. double v = MaxValue * ((double)mouseEvent.Position.X - _barStartsAt) / (_barWidth - 1);
  109. Value = Math.Clamp ((int)v, 0, MaxValue);
  110. }
  111. mouseEvent.Handled = true;
  112. SetFocus ();
  113. }
  114. return mouseEvent.Handled;
  115. }
  116. /// <summary>
  117. /// When overriden in a derived class, returns the <see cref="Color"/> to
  118. /// render at <paramref name="fraction"/> proportion of the full bars width.
  119. /// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100.
  120. /// </summary>
  121. /// <param name="fraction"></param>
  122. /// <returns></returns>
  123. protected abstract Color GetColor (double fraction);
  124. /// <summary>
  125. /// The maximum value allowed for this component e.g. Saturation allows up to 100 as it
  126. /// is a percentage while Hue allows up to 360 as it is measured in degrees.
  127. /// </summary>
  128. protected abstract int MaxValue { get; }
  129. /// <summary>
  130. /// The last drawn location in View's viewport where the Triangle appeared.
  131. /// Used exclusively for tests.
  132. /// </summary>
  133. internal int TrianglePosition { get; private set; }
  134. private bool? Adjust (int delta)
  135. {
  136. var change = (int)(delta * _cellValue);
  137. // Ensure that the change is at least 1 or -1 if delta is non-zero
  138. if (change == 0 && delta != 0)
  139. {
  140. change = delta > 0 ? 1 : -1;
  141. }
  142. Value += change;
  143. return true;
  144. }
  145. private void DrawBar (int xOffset, int yOffset, int width)
  146. {
  147. for (var x = 0; x < width; x++)
  148. {
  149. double fraction = (double)x / (width - 1);
  150. Color color = GetColor (fraction);
  151. // Adjusted isSelectedCell calculation
  152. double cellBottomThreshold = (x - 1) * _cellValue;
  153. double cellTopThreshold = x * _cellValue;
  154. if (x == width - 1)
  155. {
  156. cellTopThreshold = MaxValue;
  157. }
  158. bool isSelectedCell = Value > cellBottomThreshold && Value <= cellTopThreshold;
  159. // Check the brightness of the background color
  160. double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
  161. Color triangleColor = Color.Black;
  162. if (brightness < 0.15) // Threshold to determine if the color is too close to black
  163. {
  164. triangleColor = Color.DarkGray;
  165. }
  166. if (isSelectedCell)
  167. {
  168. // Draw the triangle at the closest position
  169. SetAttribute (new (triangleColor, color, Enabled ? TextStyle.None : TextStyle.Faint));
  170. AddRune (x + xOffset, yOffset, new ('▲'));
  171. // Record for tests
  172. TrianglePosition = x + xOffset;
  173. }
  174. else
  175. {
  176. SetAttribute (new (color, color, Enabled ? TextStyle.None : TextStyle.Faint));
  177. AddRune (x + xOffset, yOffset, new (' '));
  178. }
  179. }
  180. }
  181. private void OnValueChanged ()
  182. {
  183. ValueChanged?.Invoke (this, new (in _value));
  184. SetNeedsDraw ();
  185. }
  186. private bool? SetMax ()
  187. {
  188. Value = MaxValue;
  189. return true;
  190. }
  191. private bool? SetZero ()
  192. {
  193. Value = 0;
  194. return true;
  195. }
  196. }