// // // Pending: // - Check for NeedDisplay on the hierarchy and repaint // - Layout support // // Optimziations // - Add rendering limitation to the exposed area using System; using System.Collections; using System.Collections.Generic; namespace Terminal { public class Responder { public virtual bool CanFocus { get; set; } public bool HasFocus { get; internal set; } // Key handling public virtual void KeyDown (Event.Key kb) { } // Mouse events public virtual void MouseEvent (Event.Mouse me) { } } public class View : Responder, IEnumerable { View container = null; View focused = null; public static ConsoleDriver Driver = Application.Driver; public static IList empty = new List (0).AsReadOnly (); List subviews; public IList Subviews => subviews == null ? empty : subviews.AsReadOnly (); internal bool NeedDisplay { get; private set; } = true; // The frame for the object Rect frame; // The frame for this view public Rect Frame { get => frame; set { frame = value; SetNeedsDisplay (); } } public IEnumerator GetEnumerator () { foreach (var v in subviews) yield return v; } public Rect Bounds { get => new Rect (Point.Empty, Frame.Size); set { Frame = new Rect (frame.Location, value.Size); } } public View (Rect frame) { this.Frame = frame; CanFocus = false; } /// /// Invoke to flag that this view needs to be redisplayed, by any code /// that alters the state of the view. /// public void SetNeedsDisplay () { NeedDisplay = true; if (container != null) container.SetNeedsDisplay (); } /// /// Adds a subview to this view. /// /// /// public virtual void Add (View view) { if (view == null) return; if (subviews == null) subviews = new List (); subviews.Add (view); view.container = this; if (view.CanFocus) CanFocus = true; } /// /// Removes all the widgets from this container. /// /// /// public virtual void RemoveAll () { if (subviews == null) return; while (subviews.Count > 0) { var view = subviews [0]; Remove (view); subviews.RemoveAt (0); } } /// /// Removes a widget from this container. /// /// /// public virtual void Remove (View view) { if (view == null) return; subviews.Remove (view); view.container = null; if (subviews.Count < 1) this.CanFocus = false; } /// /// Clears the view region with the current color. /// /// /// /// This clears the entire region used by this view. /// /// public void Clear () { var h = Frame.Height; var w = Frame.Width; for (int line = 0; line < h; line++) { Move (0, line); for (int col = 0; col < w; col++) Driver.AddCh (' '); } } /// /// Converts the (col,row) position from the view into a screen (col,row). The values are clamped to (0..ScreenDim-1) /// /// View-based column. /// View-based row. /// Absolute column, display relative. /// Absolute row, display relative. internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) { // Computes the real row, col relative to the screen. rrow = row + frame.Y; rcol = col + frame.X; var ccontainer = container; while (ccontainer != null) { rrow += ccontainer.frame.Y; rcol += ccontainer.frame.X; ccontainer = ccontainer.container; } // The following ensures that the cursor is always in the screen boundaries. if (clipped) { rrow = Math.Max (0, Math.Min (rrow, Driver.Rows - 1)); rcol = Math.Max (0, Math.Min (rcol, Driver.Cols - 1)); } } Rect RectToScreen (Rect rect) { ViewToScreen (rect.X, rect.Y, out var x, out var y, clipped: false); return new Rect (x, y, rect.Width, rect.Height); } Rect ScreenClip (Rect rect) { var x = rect.X < 0 ? 0 : rect.X; var y = rect.Y < 0 ? 0 : rect.Y; var w = rect.X + rect.Width >= Driver.Cols ? Driver.Cols - rect.X : rect.Width; var h = rect.Y + rect.Height >= Driver.Rows ? Driver.Rows - rect.Y : rect.Height; return new Rect (x, y, w, h); } /// /// Draws a frame in the current view, clipped by the boundary of this view /// /// Rectangular region for the frame to be drawn. /// If set to true it fill will the contents. public void DrawFrame (Rect rect, bool fill = false) { var scrRect = RectToScreen (rect); var savedClip = Driver.Clip; Driver.Clip = ScreenClip (RectToScreen (Bounds)); Driver.DrawFrame (scrRect, fill); Driver.Clip = savedClip; } /// /// This moves the cursor to the specified column and row in the view. /// /// The move. /// Col. /// Row. public void Move (int col, int row) { ViewToScreen (col, row, out var rcol, out var rrow); Driver.Move (rcol, rrow); } /// /// Positions the cursor in the right position based on the currently focused view in the chain. /// public virtual void PositionCursor () { if (focused != null) focused.PositionCursor (); else Move (frame.X, frame.Y); } /// /// Displays the specified character in the specified column and row. /// /// Col. /// Row. /// Ch. public void AddCh (int col, int row, int ch) { if (row < 0 || col < 0) return; if (row > frame.Height - 1 || col > frame.Width - 1) return; Move (col, row); Driver.AddCh (ch); } /// /// Performs a redraw of this view and its subviews, only redraws the views that have been flagged for a re-display. /// public virtual void Redraw (Rect region) { var clipRect = new Rect (Point.Empty, frame.Size); if (subviews != null) { foreach (var view in subviews) { if (view.NeedDisplay) { if (view.Frame.IntersectsWith (clipRect) && view.Frame.IntersectsWith (region)) { // TODO: optimize this by computing the intersection of region and view.Bounds view.Redraw (view.Bounds); } view.NeedDisplay = false; } } } NeedDisplay = false; } /// /// Focuses the specified sub-view. /// /// View. public void SetFocus (View view) { if (view == null) return; if (!view.CanFocus) return; if (focused == view) return; // Make sure that this view is a subview View c; for (c = view.container; c != null; c = c.container) if (c == this) break; if (c == null) throw new ArgumentException ("the specified view is not part of the hierarchy of this view"); if (focused != null) focused.HasFocus = false; focused = view; view.HasFocus = true; if (view != null) view.EnsureFocus (); focused.PositionCursor (); } /// /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing. /// public void EnsureFocus () { if (focused == null) FocusFirst (); } /// /// Focuses the first focusable subview if one exists. /// public void FocusFirst () { foreach (var view in subviews) { if (view.CanFocus) { SetFocus (view); return; } } } /// /// Focuses the last focusable subview if one exists. /// public void FocusLast () { for (int i = subviews.Count; i > 0;) { i--; View v = subviews [i]; if (v.CanFocus) { SetFocus (v); return; } } } /// /// Focuses the previous view. /// /// true, if previous was focused, false otherwise. public bool FocusPrev () { if (focused == null) { FocusLast (); return true; } int focused_idx = -1; for (int i = subviews.Count; i > 0;) { i--; View w = subviews [i]; if (w.HasFocus) { if (w.FocusPrev ()) return true; focused_idx = i; continue; } if (w.CanFocus && focused_idx != -1) { focused.HasFocus = false; if (w.CanFocus) w.FocusLast (); SetFocus (w); return true; } } if (focused != null) { focused.HasFocus = false; focused = null; } return false; } /// /// Focuses the next view. /// /// true, if next was focused, false otherwise. public bool FocusNext () { if (focused == null) { FocusFirst (); return focused != null; } int n = subviews.Count; int focused_idx = -1; for (int i = 0; i < n; i++) { View w = subviews [i]; if (w.HasFocus) { if (w.FocusNext ()) return true; focused_idx = i; continue; } if (w.CanFocus && focused_idx != -1) { focused.HasFocus = false; if (w != null && w.CanFocus) w.FocusFirst (); SetFocus (w); return true; } } if (focused != null) { focused.HasFocus = false; focused = null; } return false; } public virtual void LayoutSubviews () { } } /// /// Toplevel views can be modally executed. /// public class Toplevel : View { public bool Running; public Toplevel (Rect frame) : base (frame) { } public static Toplevel Create () { return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows)); } #if false public override void Redraw () { base.Redraw (); for (int i = 0; i < Driver.Cols; i++) { Driver.Move (0, i); Driver.AddStr ("Line: " + i); } } #endif } /// /// A toplevel view that draws a frame around its region /// public class Window : Toplevel, IEnumerable { View contentView; string title; public string Title { get => title; set { title = value; SetNeedsDisplay (); } } public Window (Rect frame, string title = null) : base (frame) { this.Title = title; frame.Inflate (-1, -1); contentView = new View (frame); base.Add(contentView); } public IEnumerator GetEnumerator () { return contentView.GetEnumerator (); } void DrawFrame () { DrawFrame (new Rect(0, 0, Frame.Width, Frame.Height), true); } public override void Add (View view) { contentView.Add (view); } public override void Redraw (Rect bounds) { Driver.SetAttribute (Colors.Base.Normal); DrawFrame (); if (HasFocus) Driver.SetAttribute (Colors.Dialog.Normal); var width = Frame.Width; if (Title != null && width > 4) { Move (1, 0); Driver.AddCh (' '); var str = Title.Length > width ? Title.Substring (0, width - 4) : Title; Driver.AddStr (str); Driver.AddCh (' '); } Driver.SetAttribute (Colors.Dialog.Normal); contentView.Redraw (contentView.Bounds); } } public class Application { public static ConsoleDriver Driver = new CursesDriver (); public static Toplevel Top { get; private set; } public static Mono.Terminal.MainLoop MainLoop { get; private set; } static Stack toplevels = new Stack (); static Responder focus; /// /// This event is raised on each iteration of the /// main loop. /// /// /// See also /// static public event EventHandler Iteration; public static void MakeFirstResponder (Responder newResponder) { if (newResponder == null) throw new ArgumentNullException (); throw new NotImplementedException (); } /// /// Initializes the Application /// public static void Init () { if (Top != null) return; Driver.Init (); MainLoop = new Mono.Terminal.MainLoop (); Top = Toplevel.Create (); focus = Top; MainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => { //ProcessChar (); return true; }); } public class RunState : IDisposable { internal RunState (Toplevel view) { Toplevel = view; } internal Toplevel Toplevel; public void Dispose () { Dispose (true); GC.SuppressFinalize(this); } public virtual void Dispose (bool disposing) { if (Toplevel != null){ Application.End (Toplevel); Toplevel = null; } } } static public RunState Begin (Toplevel toplevel) { if (toplevel == null) throw new ArgumentNullException (nameof(toplevel)); var rs = new RunState (toplevel); Init (); Driver.PrepareToRun (); toplevels.Push (toplevel); toplevel.LayoutSubviews (); toplevel.FocusFirst (); Redraw (toplevel); toplevel.PositionCursor (); Driver.Refresh (); return rs; } static public void End (RunState rs) { if (rs == null) throw new ArgumentNullException (nameof (rs)); rs.Dispose (); } static void Shutdown () { Driver.End (); } static void Redraw (View view) { view.Redraw (view.Bounds); Driver.Refresh (); } static void Refresh (View view) { view.Redraw (view.Bounds); Driver.Refresh (); } public static void Refresh () { Driver.RedrawTop (); View last = null; foreach (var v in toplevels){ v.Redraw (v.Bounds); last = v; } if (last != null) last.PositionCursor (); Driver.Refresh (); } internal static void End (View view) { if (toplevels.Peek () != view) throw new ArgumentException ("The view that you end with must be balanced"); toplevels.Pop (); if (toplevels.Count == 0) Shutdown (); else Refresh (); } /// /// Runs the main loop for the created dialog /// /// /// Use the wait parameter to control whether this is a /// blocking or non-blocking call. /// public static void RunLoop(RunState state, bool wait = true) { if (state == null) throw new ArgumentNullException(nameof(state)); if (state.Toplevel == null) throw new ObjectDisposedException("state"); for (state.Toplevel.Running = true; state.Toplevel.Running;) { if (MainLoop.EventsPending(wait)){ MainLoop.MainIteration(); if (Iteration != null) Iteration(null, EventArgs.Empty); } else if (wait == false) return; if (state.Toplevel.NeedDisplay) state.Toplevel.Redraw (state.Toplevel.Bounds); } } public static void Run () { Run (Top); } /// /// Runs the main loop on the given container. /// /// /// This method is used to start processing events /// for the main application, but it is also used to /// run modal dialog boxes. /// public static void Run (Toplevel view) { var runToken = Begin (view); RunLoop (runToken); End (runToken); } } }