LineDrawing.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Terminal.Gui;
  6. namespace UICatalog.Scenarios;
  7. public interface ITool
  8. {
  9. void OnMouseEvent (DrawingArea area, MouseEventArgs mouseEvent);
  10. }
  11. internal class DrawLineTool : ITool
  12. {
  13. private StraightLine _currentLine;
  14. public LineStyle LineStyle { get; set; } = LineStyle.Single;
  15. /// <inheritdoc/>
  16. public void OnMouseEvent (DrawingArea area, MouseEventArgs mouseEvent)
  17. {
  18. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  19. {
  20. if (_currentLine == null)
  21. {
  22. // Mouse pressed down
  23. _currentLine = new (
  24. mouseEvent.Position,
  25. 0,
  26. Orientation.Vertical,
  27. LineStyle,
  28. area.CurrentAttribute
  29. );
  30. area.CurrentLayer.AddLine (_currentLine);
  31. }
  32. else
  33. {
  34. // Mouse dragged
  35. Point start = _currentLine.Start;
  36. Point end = mouseEvent.Position;
  37. var orientation = Orientation.Vertical;
  38. int length = end.Y - start.Y;
  39. // if line is wider than it is tall switch to horizontal
  40. if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y))
  41. {
  42. orientation = Orientation.Horizontal;
  43. length = end.X - start.X;
  44. }
  45. if (length > 0)
  46. {
  47. length++;
  48. }
  49. else
  50. {
  51. length--;
  52. }
  53. _currentLine.Length = length;
  54. _currentLine.Orientation = orientation;
  55. area.CurrentLayer.ClearCache ();
  56. area.SetNeedsDisplay ();
  57. }
  58. }
  59. else
  60. {
  61. // Mouse released
  62. if (_currentLine != null)
  63. {
  64. if (_currentLine.Length == 0)
  65. {
  66. _currentLine.Length = 1;
  67. }
  68. if (_currentLine.Style == LineStyle.None)
  69. {
  70. // Treat none as eraser
  71. int idx = area.Layers.IndexOf (area.CurrentLayer);
  72. area.Layers.Remove (area.CurrentLayer);
  73. area.CurrentLayer = new (
  74. area.CurrentLayer.Lines.Exclude (
  75. _currentLine.Start,
  76. _currentLine.Length,
  77. _currentLine.Orientation
  78. )
  79. );
  80. area.Layers.Insert (idx, area.CurrentLayer);
  81. }
  82. _currentLine = null;
  83. area.ClearUndo ();
  84. area.SetNeedsDisplay ();
  85. }
  86. }
  87. mouseEvent.Handled = true;
  88. }
  89. }
  90. [ScenarioMetadata ("Line Drawing", "Demonstrates LineCanvas.")]
  91. [ScenarioCategory ("Controls")]
  92. [ScenarioCategory ("Drawing")]
  93. public class LineDrawing : Scenario
  94. {
  95. public override void Main ()
  96. {
  97. Application.Init ();
  98. var win = new Window { Title = GetQuitKeyAndName () };
  99. var canvas = new DrawingArea { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
  100. var tools = new ToolsView { Title = "Tools", X = Pos.Right (canvas) - 20, Y = 2 };
  101. tools.ColorChanged += (s, e) => canvas.SetAttribute (e);
  102. tools.SetStyle += b => canvas.CurrentTool = new DrawLineTool { LineStyle = b };
  103. tools.AddLayer += () => canvas.AddLayer ();
  104. win.Add (canvas);
  105. win.Add (tools);
  106. tools.CurrentColor = canvas.GetNormalColor ();
  107. canvas.CurrentAttribute = tools.CurrentColor;
  108. win.KeyDown += (s, e) => { e.Handled = canvas.NewKeyDownEvent (e); };
  109. Application.Run (win);
  110. win.Dispose ();
  111. Application.Shutdown ();
  112. }
  113. public static bool PromptForColor (string title, Color current, out Color newColor)
  114. {
  115. var accept = false;
  116. var d = new Dialog
  117. {
  118. Title = title,
  119. Width = Application.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)),
  120. Height = 10
  121. };
  122. var btnOk = new Button
  123. {
  124. X = Pos.Center () - 5,
  125. Y = Application.Force16Colors ? 6 : 4,
  126. Text = "Ok",
  127. Width = Dim.Auto (),
  128. IsDefault = true
  129. };
  130. btnOk.Accepting += (s, e) =>
  131. {
  132. accept = true;
  133. e.Cancel = true;
  134. Application.RequestStop ();
  135. };
  136. var btnCancel = new Button
  137. {
  138. X = Pos.Center () + 5,
  139. Y = 4,
  140. Text = "Cancel",
  141. Width = Dim.Auto ()
  142. };
  143. btnCancel.Accepting += (s, e) =>
  144. {
  145. e.Cancel = true;
  146. Application.RequestStop ();
  147. };
  148. d.Add (btnOk);
  149. d.Add (btnCancel);
  150. d.AddButton (btnOk);
  151. d.AddButton (btnCancel);
  152. View cp;
  153. if (Application.Force16Colors)
  154. {
  155. cp = new ColorPicker16
  156. {
  157. SelectedColor = current.GetClosestNamedColor16 (),
  158. Width = Dim.Fill ()
  159. };
  160. }
  161. else
  162. {
  163. cp = new ColorPicker
  164. {
  165. SelectedColor = current,
  166. Width = Dim.Fill (),
  167. Style = new () { ShowColorName = true, ShowTextFields = true }
  168. };
  169. ((ColorPicker)cp).ApplyStyleChanges ();
  170. }
  171. d.Add (cp);
  172. Application.Run (d);
  173. d.Dispose ();
  174. newColor = Application.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor;
  175. return accept;
  176. }
  177. }
  178. public class ToolsView : Window
  179. {
  180. private Button _addLayerBtn;
  181. private readonly AttributeView _colors;
  182. private RadioGroup _stylePicker;
  183. public Attribute CurrentColor
  184. {
  185. get => _colors.Value;
  186. set => _colors.Value = value;
  187. }
  188. public ToolsView ()
  189. {
  190. BorderStyle = LineStyle.Dotted;
  191. Border.Thickness = new (1, 2, 1, 1);
  192. Initialized += ToolsView_Initialized;
  193. _colors = new ();
  194. }
  195. public event Action AddLayer;
  196. public override void BeginInit ()
  197. {
  198. base.BeginInit ();
  199. _colors.ValueChanged += (s, e) => ColorChanged?.Invoke (this, e);
  200. _stylePicker = new()
  201. {
  202. X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray ()
  203. };
  204. _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); };
  205. _stylePicker.SelectedItem = 1;
  206. _addLayerBtn = new() { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
  207. _addLayerBtn.Accepting += (s, a) => AddLayer?.Invoke ();
  208. Add (_colors, _stylePicker, _addLayerBtn);
  209. }
  210. public event EventHandler<Attribute> ColorChanged;
  211. public event Action<LineStyle> SetStyle;
  212. private void ToolsView_Initialized (object sender, EventArgs e)
  213. {
  214. Width = Math.Max (_colors.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal;
  215. Height = _colors.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical;
  216. }
  217. }
  218. public class DrawingArea : View
  219. {
  220. public readonly List<LineCanvas> Layers = new ();
  221. private readonly Stack<StraightLine> _undoHistory = new ();
  222. public Attribute CurrentAttribute { get; set; }
  223. public LineCanvas CurrentLayer { get; set; }
  224. public ITool CurrentTool { get; set; } = new DrawLineTool ();
  225. public DrawingArea () { AddLayer (); }
  226. protected override bool OnDrawingContent (Rectangle viewport)
  227. {
  228. foreach (LineCanvas canvas in Layers)
  229. {
  230. foreach (KeyValuePair<Point, Cell?> c in canvas.GetCellMap ())
  231. {
  232. if (c.Value is { })
  233. {
  234. SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal);
  235. // TODO: #2616 - Support combining sequences that don't normalize
  236. AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune);
  237. }
  238. }
  239. }
  240. // TODO: This is a hack to work around overlapped views not drawing correctly.
  241. // without this the toolbox disappears
  242. SuperView?.SetNeedsLayout();
  243. return true;
  244. }
  245. //// BUGBUG: Why is this not handled by a key binding???
  246. protected override bool OnKeyDown (Key e)
  247. {
  248. // BUGBUG: These should be implemented with key bindings
  249. if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask))
  250. {
  251. StraightLine pop = CurrentLayer.RemoveLastLine ();
  252. if (pop != null)
  253. {
  254. _undoHistory.Push (pop);
  255. SetNeedsDisplay ();
  256. return true;
  257. }
  258. }
  259. if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask))
  260. {
  261. if (_undoHistory.Any ())
  262. {
  263. StraightLine pop = _undoHistory.Pop ();
  264. CurrentLayer.AddLine (pop);
  265. SetNeedsDisplay ();
  266. return true;
  267. }
  268. }
  269. return false;
  270. }
  271. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  272. {
  273. CurrentTool.OnMouseEvent (this, mouseEvent);
  274. return mouseEvent.Handled;
  275. }
  276. internal void AddLayer ()
  277. {
  278. CurrentLayer = new ();
  279. Layers.Add (CurrentLayer);
  280. }
  281. internal void SetAttribute (Attribute a) { CurrentAttribute = a; }
  282. public void ClearUndo () { _undoHistory.Clear (); }
  283. }
  284. public class AttributeView : View
  285. {
  286. public event EventHandler<Attribute> ValueChanged;
  287. private Attribute _value;
  288. public Attribute Value
  289. {
  290. get => _value;
  291. set
  292. {
  293. _value = value;
  294. ValueChanged?.Invoke (this, value);
  295. }
  296. }
  297. private static readonly HashSet<(int, int)> ForegroundPoints = new()
  298. {
  299. (0, 0), (1, 0), (2, 0),
  300. (0, 1), (1, 1), (2, 1)
  301. };
  302. private static readonly HashSet<(int, int)> BackgroundPoints = new()
  303. {
  304. (3, 1),
  305. (1, 2), (2, 2), (3, 2)
  306. };
  307. public AttributeView ()
  308. {
  309. Width = 4;
  310. Height = 3;
  311. }
  312. /// <inheritdoc/>
  313. protected override bool OnDrawingContent (Rectangle viewport)
  314. {
  315. Color fg = Value.Foreground;
  316. Color bg = Value.Background;
  317. bool isTransparentFg = fg == GetNormalColor ().Background;
  318. bool isTransparentBg = bg == GetNormalColor ().Background;
  319. SetAttribute (new (fg, isTransparentFg ? Color.Gray : fg));
  320. // Square of foreground color
  321. foreach ((int, int) point in ForegroundPoints)
  322. {
  323. // Make pattern like this when it is same color as background of control
  324. /*▓▒
  325. ▒▓*/
  326. Rune rune;
  327. if (isTransparentFg)
  328. {
  329. rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
  330. }
  331. else
  332. {
  333. rune = (Rune)'█';
  334. }
  335. AddRune (point.Item1, point.Item2, rune);
  336. }
  337. SetAttribute (new (bg, isTransparentBg ? Color.Gray : bg));
  338. // Square of background color
  339. foreach ((int, int) point in BackgroundPoints)
  340. {
  341. // Make pattern like this when it is same color as background of control
  342. /*▓▒
  343. ▒▓*/
  344. Rune rune;
  345. if (isTransparentBg)
  346. {
  347. rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
  348. }
  349. else
  350. {
  351. rune = (Rune)'█';
  352. }
  353. AddRune (point.Item1, point.Item2, rune);
  354. }
  355. return true;
  356. }
  357. /// <inheritdoc/>
  358. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  359. {
  360. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
  361. {
  362. if (IsForegroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
  363. {
  364. ClickedInForeground ();
  365. }
  366. else if (IsBackgroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
  367. {
  368. ClickedInBackground ();
  369. }
  370. mouseEvent.Handled = true;
  371. }
  372. return mouseEvent.Handled;
  373. }
  374. private bool IsForegroundPoint (int x, int y) { return ForegroundPoints.Contains ((x, y)); }
  375. private bool IsBackgroundPoint (int x, int y) { return BackgroundPoints.Contains ((x, y)); }
  376. private void ClickedInBackground ()
  377. {
  378. if (LineDrawing.PromptForColor ("Background", Value.Background, out Color newColor))
  379. {
  380. Value = new (Value.Foreground, newColor);
  381. SetNeedsDisplay ();
  382. }
  383. }
  384. private void ClickedInForeground ()
  385. {
  386. if (LineDrawing.PromptForColor ("Foreground", Value.Foreground, out Color newColor))
  387. {
  388. Value = new (newColor, Value.Background);
  389. SetNeedsDisplay ();
  390. }
  391. }
  392. }