2
0

Core.cs 20 KB

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