Core.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. //
  2. //
  3. // Pending:
  4. // - Check for NeedDisplay on the hierarchy and repaint
  5. // - Layout support
  6. //
  7. // Optimziations
  8. // - Add rendering limitation to the exposed area
  9. using System;
  10. using System.Collections.Generic;
  11. namespace Terminal {
  12. public class Responder {
  13. public virtual bool CanFocus { get; set; }
  14. public bool HasFocus { get; internal set; }
  15. // Key handling
  16. public virtual void KeyDown (Event.Key kb) { }
  17. // Mouse events
  18. public virtual void MouseEvent (Event.Mouse me) { }
  19. }
  20. public class View : Responder {
  21. View container = null;
  22. View focused = null;
  23. public static ConsoleDriver Driver = Application.Driver;
  24. public static IList<View> empty = new List<View> (0).AsReadOnly ();
  25. List<View> subviews;
  26. public IList<View> Subviews => subviews == null ? empty : subviews.AsReadOnly ();
  27. internal bool NeedDisplay { get; private set; } = true;
  28. // The frame for the object
  29. Rect frame;
  30. // The offset of the first child view inside the view
  31. Point offset;
  32. // The frame for this view
  33. public Rect Frame {
  34. get => frame;
  35. set {
  36. frame = value;
  37. SetNeedsDisplay ();
  38. }
  39. }
  40. public Rect Bounds {
  41. get => new Rect (Point.Empty, Frame.Size);
  42. set {
  43. Frame = new Rect (frame.Location, value.Size);
  44. }
  45. }
  46. public View (Rect frame)
  47. {
  48. this.Frame = frame;
  49. CanFocus = false;
  50. }
  51. /// <summary>
  52. /// Invoke to flag that this view needs to be redisplayed, by any code
  53. /// that alters the state of the view.
  54. /// </summary>
  55. public void SetNeedsDisplay ()
  56. {
  57. NeedDisplay = true;
  58. if (container != null)
  59. container.SetNeedsDisplay ();
  60. }
  61. /// <summary>
  62. /// Adds a subview to this view.
  63. /// </summary>
  64. /// <remarks>
  65. /// </remarks>
  66. public void Add (View view)
  67. {
  68. if (view == null)
  69. return;
  70. if (subviews == null)
  71. subviews = new List<View> ();
  72. subviews.Add (view);
  73. view.container = this;
  74. if (view.CanFocus)
  75. CanFocus = true;
  76. }
  77. /// <summary>
  78. /// Removes all the widgets from this container.
  79. /// </summary>
  80. /// <remarks>
  81. /// </remarks>
  82. public virtual void RemoveAll ()
  83. {
  84. if (subviews == null)
  85. return;
  86. while (subviews.Count > 0) {
  87. var view = subviews [0];
  88. Remove (view);
  89. subviews.RemoveAt (0);
  90. }
  91. }
  92. /// <summary>
  93. /// Removes a widget from this container.
  94. /// </summary>
  95. /// <remarks>
  96. /// </remarks>
  97. public virtual void Remove (View view)
  98. {
  99. if (view == null)
  100. return;
  101. subviews.Remove (view);
  102. view.container = null;
  103. if (subviews.Count < 1)
  104. this.CanFocus = false;
  105. }
  106. /// <summary>
  107. /// Clears the view region with the current color.
  108. /// </summary>
  109. /// <remarks>
  110. /// <para>
  111. /// This clears the entire region used by this view.
  112. /// </para>
  113. /// </remarks>
  114. public void Clear ()
  115. {
  116. var h = Frame.Height;
  117. var w = Frame.Width;
  118. for (int line = 0; line < h; line++) {
  119. Move (0, line);
  120. for (int col = 0; col < w; col++)
  121. Driver.AddCh (' ');
  122. }
  123. }
  124. /// <summary>
  125. /// Converts the (col,row) position from the view into a screen (col,row). The values are clamped to (0..ScreenDim-1)
  126. /// </summary>
  127. /// <param name="col">View-based column.</param>
  128. /// <param name="row">View-based row.</param>
  129. /// <param name="rcol">Absolute column, display relative.</param>
  130. /// <param name="rrow">Absolute row, display relative.</param>
  131. internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true)
  132. {
  133. // Computes the real row, col relative to the screen.
  134. rrow = row + frame.X;
  135. rcol = col + frame.Y;
  136. var ccontainer = container;
  137. while (ccontainer != null) {
  138. rrow += ccontainer.frame.Y;
  139. rcol += ccontainer.frame.X;
  140. ccontainer = ccontainer.container;
  141. }
  142. // The following ensures that the cursor is always in the screen boundaries.
  143. if (clipped) {
  144. rrow = Math.Max (0, Math.Min (rrow, Driver.Rows - 1));
  145. rcol = Math.Max (0, Math.Min (rcol, Driver.Cols - 1));
  146. }
  147. }
  148. Rect RectToScreen (Rect rect)
  149. {
  150. ViewToScreen (rect.X, rect.Y, out var x, out var y, clipped: false);
  151. return new Rect (x, y, rect.Width, rect.Height);
  152. }
  153. Rect ScreenClip (Rect rect)
  154. {
  155. var x = rect.X < 0 ? 0 : rect.X;
  156. var y = rect.Y < 0 ? 0 : rect.Y;
  157. var w = rect.X + rect.Width >= Driver.Cols ? Driver.Cols - rect.X : rect.Width;
  158. var h = rect.Y + rect.Height >= Driver.Rows ? Driver.Rows - rect.Y : rect.Height;
  159. return new Rect (x, y, w, h);
  160. }
  161. /// <summary>
  162. /// Draws a frame in the current view, clipped by the boundary of this view
  163. /// </summary>
  164. /// <param name="rect">Rectangular region for the frame to be drawn.</param>
  165. /// <param name="fill">If set to <c>true</c> it fill will the contents.</param>
  166. public void DrawFrame (Rect rect, bool fill = false)
  167. {
  168. var scrRect = RectToScreen (rect);
  169. var savedClip = Driver.Clip;
  170. Driver.Clip = ScreenClip (RectToScreen (Bounds));
  171. Driver.DrawFrame (scrRect, fill);
  172. Driver.Clip = savedClip;
  173. }
  174. /// <summary>
  175. /// This moves the cursor to the specified column and row in the view.
  176. /// </summary>
  177. /// <returns>The move.</returns>
  178. /// <param name="col">Col.</param>
  179. /// <param name="row">Row.</param>
  180. public void Move (int col, int row)
  181. {
  182. ViewToScreen (col, row, out var rcol, out var rrow);
  183. Driver.Move (rcol, rrow);
  184. }
  185. /// <summary>
  186. /// Positions the cursor in the right position based on the currently focused view in the chain.
  187. /// </summary>
  188. public virtual void PositionCursor ()
  189. {
  190. if (focused != null)
  191. focused.PositionCursor ();
  192. else
  193. Move (frame.X, frame.Y);
  194. }
  195. /// <summary>
  196. /// Displays the specified character in the specified column and row.
  197. /// </summary>
  198. /// <param name="col">Col.</param>
  199. /// <param name="row">Row.</param>
  200. /// <param name="ch">Ch.</param>
  201. public void AddCh (int col, int row, int ch)
  202. {
  203. if (row < 0 || col < 0)
  204. return;
  205. if (row > frame.Height - 1 || col > frame.Width - 1)
  206. return;
  207. Move (col, row);
  208. Driver.AddCh (ch);
  209. }
  210. /// <summary>
  211. /// Performs a redraw of this view and its subviews, only redraws the views that have been flagged for a re-display.
  212. /// </summary>
  213. public virtual void Redraw ()
  214. {
  215. var clipRect = new Rect (offset, frame.Size);
  216. if (subviews != null) {
  217. foreach (var view in subviews) {
  218. if (view.NeedDisplay) {
  219. if (view.Frame.IntersectsWith (clipRect)) {
  220. view.Redraw ();
  221. }
  222. view.NeedDisplay = false;
  223. }
  224. }
  225. }
  226. NeedDisplay = false;
  227. }
  228. /// <summary>
  229. /// Focuses the specified sub-view.
  230. /// </summary>
  231. /// <param name="view">View.</param>
  232. public void SetFocus (View view)
  233. {
  234. if (view == null)
  235. return;
  236. if (!view.CanFocus)
  237. return;
  238. if (focused == view)
  239. return;
  240. // Make sure that this view is a subview
  241. View c;
  242. for (c = view.container; c != null; c = c.container)
  243. if (c == this)
  244. break;
  245. if (c == null)
  246. throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
  247. if (focused != null)
  248. focused.HasFocus = false;
  249. focused = view;
  250. view.HasFocus = true;
  251. if (view != null)
  252. view.EnsureFocus ();
  253. focused.PositionCursor ();
  254. }
  255. /// <summary>
  256. /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing.
  257. /// </summary>
  258. public void EnsureFocus ()
  259. {
  260. if (focused == null)
  261. FocusFirst ();
  262. }
  263. /// <summary>
  264. /// Focuses the first focusable subview if one exists.
  265. /// </summary>
  266. public void FocusFirst ()
  267. {
  268. foreach (var view in subviews) {
  269. if (view.CanFocus) {
  270. SetFocus (view);
  271. return;
  272. }
  273. }
  274. }
  275. /// <summary>
  276. /// Focuses the last focusable subview if one exists.
  277. /// </summary>
  278. public void FocusLast ()
  279. {
  280. for (int i = subviews.Count; i > 0;) {
  281. i--;
  282. View v = subviews [i];
  283. if (v.CanFocus) {
  284. SetFocus (v);
  285. return;
  286. }
  287. }
  288. }
  289. /// <summary>
  290. /// Focuses the previous view.
  291. /// </summary>
  292. /// <returns><c>true</c>, if previous was focused, <c>false</c> otherwise.</returns>
  293. public bool FocusPrev ()
  294. {
  295. if (focused == null) {
  296. FocusLast ();
  297. return true;
  298. }
  299. int focused_idx = -1;
  300. for (int i = subviews.Count; i > 0;) {
  301. i--;
  302. View w = subviews [i];
  303. if (w.HasFocus) {
  304. if (w.FocusPrev ())
  305. return true;
  306. focused_idx = i;
  307. continue;
  308. }
  309. if (w.CanFocus && focused_idx != -1) {
  310. focused.HasFocus = false;
  311. if (w.CanFocus)
  312. w.FocusLast ();
  313. SetFocus (w);
  314. return true;
  315. }
  316. }
  317. if (focused != null) {
  318. focused.HasFocus = false;
  319. focused = null;
  320. }
  321. return false;
  322. }
  323. /// <summary>
  324. /// Focuses the next view.
  325. /// </summary>
  326. /// <returns><c>true</c>, if next was focused, <c>false</c> otherwise.</returns>
  327. public bool FocusNext ()
  328. {
  329. if (focused == null) {
  330. FocusFirst ();
  331. return focused != null;
  332. }
  333. int n = subviews.Count;
  334. int focused_idx = -1;
  335. for (int i = 0; i < n; i++) {
  336. View w = subviews [i];
  337. if (w.HasFocus) {
  338. if (w.FocusNext ())
  339. return true;
  340. focused_idx = i;
  341. continue;
  342. }
  343. if (w.CanFocus && focused_idx != -1) {
  344. focused.HasFocus = false;
  345. if (w != null && w.CanFocus)
  346. w.FocusFirst ();
  347. SetFocus (w);
  348. return true;
  349. }
  350. }
  351. if (focused != null) {
  352. focused.HasFocus = false;
  353. focused = null;
  354. }
  355. return false;
  356. }
  357. public virtual void LayoutSubviews ()
  358. {
  359. }
  360. }
  361. /// <summary>
  362. /// Toplevel views can be modally executed.
  363. /// </summary>
  364. public class Toplevel : View {
  365. public bool Running;
  366. public Toplevel (Rect frame) : base (frame)
  367. {
  368. }
  369. public static Toplevel Create ()
  370. {
  371. return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows));
  372. }
  373. #if false
  374. public override void Redraw ()
  375. {
  376. base.Redraw ();
  377. for (int i = 0; i < Driver.Cols; i++) {
  378. Driver.Move (0, i);
  379. Driver.AddStr ("Line: " + i);
  380. }
  381. }
  382. #endif
  383. }
  384. /// <summary>
  385. /// A toplevel view that draws a frame around its region
  386. /// </summary>
  387. public class Window : Toplevel {
  388. View contentView;
  389. string title;
  390. public string Title {
  391. get => title;
  392. set {
  393. title = value;
  394. SetNeedsDisplay ();
  395. }
  396. }
  397. public Window (Rect frame, string title = null) : base (frame)
  398. {
  399. this.Title = title;
  400. frame.Inflate (-1, -1);
  401. contentView = new View (frame);
  402. Add(contentView);
  403. }
  404. void DrawFrame ()
  405. {
  406. DrawFrame (new Rect(0, 0, Frame.Width, Frame.Height), true);
  407. }
  408. public override void Redraw ()
  409. {
  410. Driver.SetColor (Colors.Base.Normal);
  411. DrawFrame ();
  412. if (HasFocus)
  413. Driver.SetColor (Colors.Dialog.Normal);
  414. var width = Frame.Width;
  415. if (Title != null && width > 4) {
  416. Move (1, 0);
  417. Driver.AddCh (' ');
  418. var str = Title.Length > width ? Title.Substring (0, width - 4) : Title;
  419. Driver.AddStr (str);
  420. Driver.AddCh (' ');
  421. }
  422. Driver.SetColor (Colors.Dialog.Normal);
  423. contentView.Redraw ();
  424. }
  425. }
  426. public class Application {
  427. public static ConsoleDriver Driver = new CursesDriver ();
  428. public static Toplevel Top { get; private set; }
  429. public static Mono.Terminal.MainLoop MainLoop { get; private set; }
  430. static Stack<View> toplevels = new Stack<View> ();
  431. static Responder focus;
  432. /// <summary>
  433. /// This event is raised on each iteration of the
  434. /// main loop.
  435. /// </summary>
  436. /// <remarks>
  437. /// See also <see cref="Timeout"/>
  438. /// </remarks>
  439. static public event EventHandler Iteration;
  440. public static void MakeFirstResponder (Responder newResponder)
  441. {
  442. if (newResponder == null)
  443. throw new ArgumentNullException ();
  444. throw new NotImplementedException ();
  445. }
  446. /// <summary>
  447. /// Initializes the Application
  448. /// </summary>
  449. public static void Init ()
  450. {
  451. if (Top != null)
  452. return;
  453. Driver.Init ();
  454. MainLoop = new Mono.Terminal.MainLoop ();
  455. Top = Toplevel.Create ();
  456. focus = Top;
  457. MainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => {
  458. //ProcessChar ();
  459. return true;
  460. });
  461. }
  462. public class RunState : IDisposable {
  463. internal RunState (Toplevel view)
  464. {
  465. Toplevel = view;
  466. }
  467. internal Toplevel Toplevel;
  468. public void Dispose ()
  469. {
  470. Dispose (true);
  471. GC.SuppressFinalize(this);
  472. }
  473. public virtual void Dispose (bool disposing)
  474. {
  475. if (Toplevel != null){
  476. Application.End (Toplevel);
  477. Toplevel = null;
  478. }
  479. }
  480. }
  481. static public RunState Begin (Toplevel toplevel)
  482. {
  483. if (toplevel == null)
  484. throw new ArgumentNullException (nameof(toplevel));
  485. var rs = new RunState (toplevel);
  486. Init ();
  487. Driver.PrepareToRun ();
  488. toplevels.Push (toplevel);
  489. toplevel.LayoutSubviews ();
  490. toplevel.FocusFirst ();
  491. Redraw (toplevel);
  492. toplevel.PositionCursor ();
  493. Driver.Refresh ();
  494. return rs;
  495. }
  496. static public void End (RunState rs)
  497. {
  498. if (rs == null)
  499. throw new ArgumentNullException (nameof (rs));
  500. rs.Dispose ();
  501. }
  502. static void Shutdown ()
  503. {
  504. Driver.End ();
  505. }
  506. static void Redraw (View view)
  507. {
  508. view.Redraw ();
  509. Driver.Refresh ();
  510. }
  511. static void Refresh (View view)
  512. {
  513. view.Redraw ();
  514. Driver.Refresh ();
  515. }
  516. public static void Refresh ()
  517. {
  518. Driver.RedrawTop ();
  519. View last = null;
  520. foreach (var v in toplevels){
  521. v.Redraw ();
  522. last = v;
  523. }
  524. if (last != null)
  525. last.PositionCursor ();
  526. Driver.Refresh ();
  527. }
  528. internal static void End (View view)
  529. {
  530. if (toplevels.Peek () != view)
  531. throw new ArgumentException ("The view that you end with must be balanced");
  532. toplevels.Pop ();
  533. if (toplevels.Count == 0)
  534. Shutdown ();
  535. else
  536. Refresh ();
  537. }
  538. /// <summary>
  539. /// Runs the main loop for the created dialog
  540. /// </summary>
  541. /// <remarks>
  542. /// Use the wait parameter to control whether this is a
  543. /// blocking or non-blocking call.
  544. /// </remarks>
  545. public static void RunLoop(RunState state, bool wait = true)
  546. {
  547. if (state == null)
  548. throw new ArgumentNullException(nameof(state));
  549. if (state.Toplevel == null)
  550. throw new ObjectDisposedException("state");
  551. for (state.Toplevel.Running = true; state.Toplevel.Running;) {
  552. if (MainLoop.EventsPending(wait)){
  553. MainLoop.MainIteration();
  554. if (Iteration != null)
  555. Iteration(null, EventArgs.Empty);
  556. } else if (wait == false)
  557. return;
  558. if (state.Toplevel.NeedDisplay)
  559. state.Toplevel.Redraw ();
  560. }
  561. }
  562. public static void Run ()
  563. {
  564. Run (Top);
  565. }
  566. /// <summary>
  567. /// Runs the main loop on the given container.
  568. /// </summary>
  569. /// <remarks>
  570. /// This method is used to start processing events
  571. /// for the main application, but it is also used to
  572. /// run modal dialog boxes.
  573. /// </remarks>
  574. public static void Run (Toplevel view)
  575. {
  576. var runToken = Begin (view);
  577. RunLoop (runToken);
  578. End (runToken);
  579. }
  580. }
  581. }