Margin.cs 10 KB

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