Margin.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. var view = stack.Pop ();
  64. if (view.Margin is { } margin && margin.Thickness != Thickness.Empty && margin.GetCachedClip () != null)
  65. {
  66. margin.NeedsDraw = true;
  67. Region? saved = view.GetClip ();
  68. view.SetClip (margin.GetCachedClip ());
  69. margin.Draw ();
  70. view.SetClip (saved);
  71. margin.ClearCachedClip ();
  72. }
  73. Debug.Assert (view.NeedsDraw == false);
  74. view.NeedsDraw = false;
  75. foreach (var subview in view.SubViews)
  76. {
  77. stack.Push (subview);
  78. }
  79. }
  80. return true;
  81. }
  82. /// <inheritdoc/>
  83. public override void BeginInit ()
  84. {
  85. base.BeginInit ();
  86. if (Parent is null)
  87. {
  88. return;
  89. }
  90. ShadowStyle = base.ShadowStyle;
  91. Parent.MouseStateChanged += OnParentOnMouseStateChanged;
  92. }
  93. /// <inheritdoc/>
  94. protected override bool OnClearingViewport ()
  95. {
  96. if (Thickness == Thickness.Empty)
  97. {
  98. return true;
  99. }
  100. Rectangle screen = ViewportToScreen (Viewport);
  101. if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || HasScheme)
  102. {
  103. // This just draws/clears the thickness, not the insides.
  104. // TODO: This is a hack. See https://github.com/gui-cs/Terminal.Gui/issues/4016
  105. //SetAttribute (GetAttributeForRole (VisualRole.Normal));
  106. Thickness.Draw (Driver, screen, Diagnostics, ToString ());
  107. }
  108. if (ShadowStyle != ShadowStyle.None)
  109. {
  110. // Don't clear where the shadow goes
  111. screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT);
  112. }
  113. return true;
  114. }
  115. /// <inheritdoc />
  116. protected override bool OnDrawingText ()
  117. {
  118. return ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent);
  119. }
  120. #region Shadow
  121. // private bool _pressed;
  122. private ShadowView? _bottomShadow;
  123. private ShadowView? _rightShadow;
  124. /// <summary>
  125. /// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
  126. /// Margin.
  127. /// </summary>
  128. public ShadowStyle SetShadow (ShadowStyle style)
  129. {
  130. if (_rightShadow is { })
  131. {
  132. Remove (_rightShadow);
  133. _rightShadow.Dispose ();
  134. _rightShadow = null;
  135. }
  136. if (_bottomShadow is { })
  137. {
  138. Remove (_bottomShadow);
  139. _bottomShadow.Dispose ();
  140. _bottomShadow = null;
  141. }
  142. if (ShadowStyle != ShadowStyle.None)
  143. {
  144. // Turn off shadow
  145. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT);
  146. }
  147. if (style != ShadowStyle.None)
  148. {
  149. // Turn on shadow
  150. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT);
  151. }
  152. if (style != ShadowStyle.None)
  153. {
  154. _rightShadow = new ()
  155. {
  156. X = Pos.AnchorEnd (SHADOW_WIDTH),
  157. Y = 0,
  158. Width = SHADOW_WIDTH,
  159. Height = Dim.Fill (),
  160. ShadowStyle = style,
  161. Orientation = Orientation.Vertical
  162. };
  163. _bottomShadow = new ()
  164. {
  165. X = 0,
  166. Y = Pos.AnchorEnd (SHADOW_HEIGHT),
  167. Width = Dim.Fill (),
  168. Height = SHADOW_HEIGHT,
  169. ShadowStyle = style,
  170. Orientation = Orientation.Horizontal
  171. };
  172. Add (_rightShadow, _bottomShadow);
  173. }
  174. return style;
  175. }
  176. /// <inheritdoc/>
  177. public override ShadowStyle ShadowStyle
  178. {
  179. get => base.ShadowStyle;
  180. set => base.ShadowStyle = SetShadow (value);
  181. }
  182. private void OnParentOnMouseStateChanged (object? sender, EventArgs<MouseState> args)
  183. {
  184. if (sender is not View parent || Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None)
  185. {
  186. return;
  187. }
  188. bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag (MouseState.Pressed);
  189. bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside); ;
  190. if (pressedOutside)
  191. {
  192. pressed = false;
  193. }
  194. if (MouseState.HasFlag (MouseState.Pressed) && !pressed)
  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. MouseState &= ~MouseState.Pressed;
  213. return;
  214. }
  215. if (!MouseState.HasFlag (MouseState.Pressed) && 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. MouseState |= MouseState.Pressed;
  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. }