Margin.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>The Margin for a <see cref="View"/>. Accessed via <see cref="View.Margin"/></summary>
  4. /// <remarks>
  5. /// <para>
  6. /// The Margin is transparent by default. This can be overriden by explicitly setting <see cref="ColorScheme"/>.
  7. /// </para>
  8. /// <para>
  9. /// Margins are drawn after all other Views in the application View hierarchy are drawn.
  10. /// </para>
  11. /// <para>See the <see cref="Adornment"/> class.</para>
  12. /// </remarks>
  13. public class Margin : Adornment
  14. {
  15. private const int SHADOW_WIDTH = 1;
  16. private const int SHADOW_HEIGHT = 1;
  17. private const int PRESS_MOVE_HORIZONTAL = 1;
  18. private const int PRESS_MOVE_VERTICAL = 0;
  19. /// <inheritdoc/>
  20. public Margin ()
  21. { /* Do nothing; A parameter-less constructor is required to support all views unit tests. */
  22. }
  23. /// <inheritdoc/>
  24. public Margin (View parent) : base (parent)
  25. {
  26. /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */
  27. // BUGBUG: We should not set HighlightStyle.Pressed here, but wherever it is actually needed
  28. // HighlightStyle |= HighlightStyle.Pressed;
  29. Highlight += Margin_Highlight;
  30. SubViewLayout += Margin_LayoutStarted;
  31. // Margin should not be focusable
  32. CanFocus = false;
  33. }
  34. // When the Parent is drawn, we cache the clip region so we can draw the Margin after all other Views
  35. // QUESTION: Why can't this just be the NeedsDisplay region?
  36. private Region? _cachedClip;
  37. internal Region? GetCachedClip () { return _cachedClip; }
  38. internal void ClearCachedClip () { _cachedClip = null; }
  39. internal void CacheClip ()
  40. {
  41. if (Thickness != Thickness.Empty)
  42. {
  43. // PERFORMANCE: How expensive are these clones?
  44. _cachedClip = GetClip ()?.Clone ();
  45. }
  46. }
  47. // PERFORMANCE: Margins are ALWAYS drawn. This may be an issue for apps that have a large number of views with shadows.
  48. /// <summary>
  49. /// INTERNAL API - Draws the margins for the specified views. This is called by the <see cref="Application"/> on each
  50. /// iteration of the main loop after all Views have been drawn.
  51. /// </summary>
  52. /// <param name="margins"></param>
  53. /// <returns><see langword="true"/></returns>
  54. internal static bool DrawMargins (IEnumerable<View> margins)
  55. {
  56. Stack<View> stack = new (margins);
  57. while (stack.Count > 0)
  58. {
  59. var view = stack.Pop ();
  60. if (view.Margin?.GetCachedClip() != null)
  61. {
  62. view.Margin.NeedsDraw = true;
  63. Region? saved = GetClip ();
  64. View.SetClip (view.Margin.GetCachedClip ());
  65. view.Margin.Draw ();
  66. View.SetClip (saved);
  67. view.Margin.ClearCachedClip ();
  68. }
  69. view.NeedsDraw = false;
  70. foreach (var subview in view.SubViews)
  71. {
  72. stack.Push (subview);
  73. }
  74. }
  75. return true;
  76. }
  77. /// <inheritdoc/>
  78. public override void BeginInit ()
  79. {
  80. base.BeginInit ();
  81. if (Parent is null)
  82. {
  83. return;
  84. }
  85. ShadowStyle = base.ShadowStyle;
  86. }
  87. /// <summary>
  88. /// The color scheme for the Margin. If set to <see langword="null"/> (the default), the margin will be transparent.
  89. /// </summary>
  90. public override ColorScheme? ColorScheme
  91. {
  92. get
  93. {
  94. if (base.ColorScheme is { })
  95. {
  96. return base.ColorScheme;
  97. }
  98. return (Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"])!;
  99. }
  100. set
  101. {
  102. base.ColorScheme = value;
  103. Parent?.SetNeedsDraw ();
  104. }
  105. }
  106. /// <inheritdoc/>
  107. protected override bool OnClearingViewport ()
  108. {
  109. if (Thickness == Thickness.Empty)
  110. {
  111. return true;
  112. }
  113. Rectangle screen = ViewportToScreen (Viewport);
  114. // This just draws/clears the thickness, not the insides.
  115. if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || base.ColorScheme is { })
  116. {
  117. Thickness.Draw (screen, Diagnostics, ToString ());
  118. }
  119. if (ShadowStyle != ShadowStyle.None)
  120. {
  121. // Don't clear where the shadow goes
  122. screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT);
  123. }
  124. return true;
  125. }
  126. #region Shadow
  127. private bool _pressed;
  128. private ShadowView? _bottomShadow;
  129. private ShadowView? _rightShadow;
  130. /// <summary>
  131. /// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
  132. /// Margin.
  133. /// </summary>
  134. public ShadowStyle SetShadow (ShadowStyle style)
  135. {
  136. if (_rightShadow is { })
  137. {
  138. Remove (_rightShadow);
  139. _rightShadow.Dispose ();
  140. _rightShadow = null;
  141. }
  142. if (_bottomShadow is { })
  143. {
  144. Remove (_bottomShadow);
  145. _bottomShadow.Dispose ();
  146. _bottomShadow = null;
  147. }
  148. if (ShadowStyle != ShadowStyle.None)
  149. {
  150. // Turn off shadow
  151. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT);
  152. }
  153. if (style != ShadowStyle.None)
  154. {
  155. // Turn on shadow
  156. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT);
  157. }
  158. if (style != ShadowStyle.None)
  159. {
  160. _rightShadow = new ()
  161. {
  162. X = Pos.AnchorEnd (SHADOW_WIDTH),
  163. Y = 0,
  164. Width = SHADOW_WIDTH,
  165. Height = Dim.Fill (),
  166. ShadowStyle = style,
  167. Orientation = Orientation.Vertical
  168. };
  169. _bottomShadow = new ()
  170. {
  171. X = 0,
  172. Y = Pos.AnchorEnd (SHADOW_HEIGHT),
  173. Width = Dim.Fill (),
  174. Height = SHADOW_HEIGHT,
  175. ShadowStyle = style,
  176. Orientation = Orientation.Horizontal
  177. };
  178. Add (_rightShadow, _bottomShadow);
  179. }
  180. return style;
  181. }
  182. /// <inheritdoc/>
  183. public override ShadowStyle ShadowStyle
  184. {
  185. get => base.ShadowStyle;
  186. set => base.ShadowStyle = SetShadow (value);
  187. }
  188. private void Margin_Highlight (object? sender, CancelEventArgs<HighlightStyle> e)
  189. {
  190. if (Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None)
  191. {
  192. return;
  193. }
  194. if (_pressed && e.NewValue == HighlightStyle.None)
  195. {
  196. // If the view is pressed and the highlight is being removed, move the shadow back.
  197. // Note, for visual effects reasons, we only move horizontally.
  198. // TODO: Add a setting or flag that lets the view move vertically as well.
  199. Thickness = new (
  200. Thickness.Left - PRESS_MOVE_HORIZONTAL,
  201. Thickness.Top - PRESS_MOVE_VERTICAL,
  202. Thickness.Right + PRESS_MOVE_HORIZONTAL,
  203. Thickness.Bottom + PRESS_MOVE_VERTICAL);
  204. if (_rightShadow is { })
  205. {
  206. _rightShadow.Visible = true;
  207. }
  208. if (_bottomShadow is { })
  209. {
  210. _bottomShadow.Visible = true;
  211. }
  212. _pressed = false;
  213. return;
  214. }
  215. if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed))
  216. {
  217. // If the view is not pressed and we want highlight move the shadow
  218. // Note, for visual effects reasons, we only move horizontally.
  219. // TODO: Add a setting or flag that lets the view move vertically as well.
  220. Thickness = new (
  221. Thickness.Left + PRESS_MOVE_HORIZONTAL,
  222. Thickness.Top + PRESS_MOVE_VERTICAL,
  223. Thickness.Right - PRESS_MOVE_HORIZONTAL,
  224. Thickness.Bottom - PRESS_MOVE_VERTICAL);
  225. _pressed = true;
  226. if (_rightShadow is { })
  227. {
  228. _rightShadow.Visible = false;
  229. }
  230. if (_bottomShadow is { })
  231. {
  232. _bottomShadow.Visible = false;
  233. }
  234. }
  235. }
  236. private void Margin_LayoutStarted (object? sender, LayoutEventArgs e)
  237. {
  238. // Adjust the shadow such that it is drawn aligned with the Border
  239. if (_rightShadow is { } && _bottomShadow is { })
  240. {
  241. switch (ShadowStyle)
  242. {
  243. case ShadowStyle.Transparent:
  244. // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner.
  245. _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0;
  246. break;
  247. case ShadowStyle.Opaque:
  248. // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner.
  249. _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0;
  250. _bottomShadow.X = Parent.Border.Thickness.Left > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).X + 1 : 0;
  251. break;
  252. case ShadowStyle.None:
  253. default:
  254. _rightShadow.Y = 0;
  255. _bottomShadow.X = 0;
  256. break;
  257. }
  258. }
  259. }
  260. #endregion Shadow
  261. }