Margin.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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 && ShadowStyle != ShadowStyle.None)
  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. // TODO: We may actualy need this. Not clear what broke, if anything by commenting it out. See https://github.com/gui-cs/Terminal.Gui/issues/4016
  88. /////// <summary>
  89. /////// The color scheme for the Margin. If set to <see langword="null"/> (the default), the margin will be transparent.
  90. /////// </summary>
  91. //public override ColorScheme? ColorScheme
  92. //{
  93. // get
  94. // {
  95. // //if (base.ColorScheme is { })
  96. // {
  97. // return base.ColorScheme;
  98. // }
  99. // //return (Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"])!;
  100. // }
  101. // set
  102. // {
  103. // base.ColorScheme = value;
  104. // Parent?.SetNeedsDraw ();
  105. // }
  106. //}
  107. /// <inheritdoc />
  108. public override Attribute GetNormalColor ()
  109. {
  110. if (_colorScheme is { })
  111. {
  112. return _colorScheme.Normal;
  113. }
  114. if (Parent is { })
  115. {
  116. return Parent.GetNormalColor ();
  117. }
  118. return base.GetNormalColor ();
  119. }
  120. /// <inheritdoc />
  121. public override Attribute GetHotNormalColor ()
  122. {
  123. if (Parent is { })
  124. {
  125. return Parent.GetHotNormalColor ();
  126. }
  127. return base.GetHotNormalColor ();
  128. }
  129. /// <inheritdoc />
  130. public override Attribute GetFocusColor ()
  131. {
  132. if (Parent is { })
  133. {
  134. return Parent.GetFocusColor ();
  135. }
  136. return base.GetFocusColor ();
  137. }
  138. /// <inheritdoc />
  139. public override Attribute GetHotFocusColor ()
  140. {
  141. if (Parent is { })
  142. {
  143. return Parent.GetHotFocusColor ();
  144. }
  145. return base.GetHotFocusColor ();
  146. }
  147. /// <inheritdoc />
  148. protected override bool OnSettingNormalAttribute ()
  149. {
  150. if (Parent is { })
  151. {
  152. SetAttribute (Parent.GetNormalColor ());
  153. return true;
  154. }
  155. return false;
  156. }
  157. /// <inheritdoc/>
  158. protected override bool OnClearingViewport ()
  159. {
  160. if (Thickness == Thickness.Empty)
  161. {
  162. return true;
  163. }
  164. Rectangle screen = ViewportToScreen (Viewport);
  165. // This just draws/clears the thickness, not the insides.
  166. if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || base.ColorScheme is { })
  167. {
  168. // TODO: This is a hack. See https://github.com/gui-cs/Terminal.Gui/issues/4016
  169. SetAttribute (GetNormalColor ());
  170. Thickness.Draw (screen, Diagnostics, ToString ());
  171. }
  172. if (ShadowStyle != ShadowStyle.None)
  173. {
  174. // Don't clear where the shadow goes
  175. screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT);
  176. }
  177. return true;
  178. }
  179. #region Shadow
  180. private bool _pressed;
  181. private ShadowView? _bottomShadow;
  182. private ShadowView? _rightShadow;
  183. /// <summary>
  184. /// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
  185. /// Margin.
  186. /// </summary>
  187. public ShadowStyle SetShadow (ShadowStyle style)
  188. {
  189. if (_rightShadow is { })
  190. {
  191. Remove (_rightShadow);
  192. _rightShadow.Dispose ();
  193. _rightShadow = null;
  194. }
  195. if (_bottomShadow is { })
  196. {
  197. Remove (_bottomShadow);
  198. _bottomShadow.Dispose ();
  199. _bottomShadow = null;
  200. }
  201. if (ShadowStyle != ShadowStyle.None)
  202. {
  203. // Turn off shadow
  204. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT);
  205. }
  206. if (style != ShadowStyle.None)
  207. {
  208. // Turn on shadow
  209. Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT);
  210. }
  211. if (style != ShadowStyle.None)
  212. {
  213. _rightShadow = new ()
  214. {
  215. X = Pos.AnchorEnd (SHADOW_WIDTH),
  216. Y = 0,
  217. Width = SHADOW_WIDTH,
  218. Height = Dim.Fill (),
  219. ShadowStyle = style,
  220. Orientation = Orientation.Vertical
  221. };
  222. _bottomShadow = new ()
  223. {
  224. X = 0,
  225. Y = Pos.AnchorEnd (SHADOW_HEIGHT),
  226. Width = Dim.Fill (),
  227. Height = SHADOW_HEIGHT,
  228. ShadowStyle = style,
  229. Orientation = Orientation.Horizontal
  230. };
  231. Add (_rightShadow, _bottomShadow);
  232. }
  233. return style;
  234. }
  235. /// <inheritdoc/>
  236. public override ShadowStyle ShadowStyle
  237. {
  238. get => base.ShadowStyle;
  239. set => base.ShadowStyle = SetShadow (value);
  240. }
  241. private void Margin_Highlight (object? sender, CancelEventArgs<HighlightStyle> e)
  242. {
  243. if (Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None)
  244. {
  245. return;
  246. }
  247. if (_pressed && e.NewValue == HighlightStyle.None)
  248. {
  249. // If the view is pressed and the highlight is being removed, move the shadow back.
  250. // Note, for visual effects reasons, we only move horizontally.
  251. // TODO: Add a setting or flag that lets the view move vertically as well.
  252. Thickness = new (
  253. Thickness.Left - PRESS_MOVE_HORIZONTAL,
  254. Thickness.Top - PRESS_MOVE_VERTICAL,
  255. Thickness.Right + PRESS_MOVE_HORIZONTAL,
  256. Thickness.Bottom + PRESS_MOVE_VERTICAL);
  257. if (_rightShadow is { })
  258. {
  259. _rightShadow.Visible = true;
  260. }
  261. if (_bottomShadow is { })
  262. {
  263. _bottomShadow.Visible = true;
  264. }
  265. _pressed = false;
  266. return;
  267. }
  268. if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed))
  269. {
  270. // If the view is not pressed and we want highlight move the shadow
  271. // Note, for visual effects reasons, we only move horizontally.
  272. // TODO: Add a setting or flag that lets the view move vertically as well.
  273. Thickness = new (
  274. Thickness.Left + PRESS_MOVE_HORIZONTAL,
  275. Thickness.Top + PRESS_MOVE_VERTICAL,
  276. Thickness.Right - PRESS_MOVE_HORIZONTAL,
  277. Thickness.Bottom - PRESS_MOVE_VERTICAL);
  278. _pressed = true;
  279. if (_rightShadow is { })
  280. {
  281. _rightShadow.Visible = false;
  282. }
  283. if (_bottomShadow is { })
  284. {
  285. _bottomShadow.Visible = false;
  286. }
  287. }
  288. }
  289. private void Margin_LayoutStarted (object? sender, LayoutEventArgs e)
  290. {
  291. // Adjust the shadow such that it is drawn aligned with the Border
  292. if (_rightShadow is { } && _bottomShadow is { })
  293. {
  294. switch (ShadowStyle)
  295. {
  296. case ShadowStyle.Transparent:
  297. // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner.
  298. _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0;
  299. break;
  300. case ShadowStyle.Opaque:
  301. // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner.
  302. _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0;
  303. _bottomShadow.X = Parent.Border.Thickness.Left > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).X + 1 : 0;
  304. break;
  305. case ShadowStyle.None:
  306. default:
  307. _rightShadow.Y = 0;
  308. _bottomShadow.X = 0;
  309. break;
  310. }
  311. }
  312. }
  313. #endregion Shadow
  314. }