Margin.cs 10 KB

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