// // Core.cs: The core engine for gui.cs // // Authors: // Miguel de Icaza (miguel@gnome.org) // // Pending: // - Check for NeedDisplay on the hierarchy and repaint // - Layout support // - "Colors" type or "Attributes" type? // - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? // // Optimziations // - Add rendering limitation to the exposed area using System; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Linq; using NStack; using System.ComponentModel; namespace Terminal.Gui { /// /// A static, singelton class provding the main application driver for Terminal.Gui apps. /// /// /// /// // A simple Terminal.Gui app that creates a window with a frame and title with /// // 5 rows/columns of padding. /// Application.Init(); /// var win = new Window ("Hello World - CTRL-Q to quit") { /// X = 5, /// Y = 5, /// Width = Dim.Fill (5), /// Height = Dim.Fill (5) /// }; /// Application.Top.Add(win); /// Application.Run(); /// /// /// /// /// Creates a instance of to process input events, handle timers and /// other sources of data. It is accessible via the property. /// /// /// You can hook up to the event to have your method /// invoked on each iteration of the . /// /// /// When invoked sets the SynchronizationContext to one that is tied /// to the mainloop, allowing user code to use async/await. /// /// public static class Application { /// /// The current in use. /// public static ConsoleDriver Driver; /// /// The object used for the application on startup () /// /// The top. public static Toplevel Top { get; private set; } /// /// The current object. This is updated when enters and leaves to point to the current . /// /// The current. public static Toplevel Current { get; private set; } /// /// The current used in the terminal. /// public static bool HeightAsBuffer { get { if (Driver == null) { throw new ArgumentNullException ("The driver must be initialized first."); } return Driver.HeightAsBuffer; } set { if (Driver == null) { throw new ArgumentNullException ("The driver must be initialized first."); } if (Driver.HeightAsBuffer != value) { Driver.HeightAsBuffer = value; } } } /// /// Used only by to forcing always moving the cursor position when writing to the screen. /// public static bool AlwaysSetPosition { get { if (Driver is NetDriver) { return (Driver as NetDriver).AlwaysSetPosition; } return false; } set { if (Driver is NetDriver) { (Driver as NetDriver).AlwaysSetPosition = value; Driver.Refresh (); } } } /// /// The driver for the application /// /// The main loop. public static MainLoop MainLoop { get; private set; } static Stack toplevels = new Stack (); /// /// This event is raised on each iteration of the /// /// /// See also /// public static Action Iteration; /// /// Returns a rectangle that is centered in the screen for the provided size. /// /// The centered rect. /// Size for the rectangle. public static Rect MakeCenteredRect (Size size) { return new Rect (new Point ((Driver.Cols - size.Width) / 2, (Driver.Rows - size.Height) / 2), size); } // // provides the sync context set while executing code in Terminal.Gui, to let // users use async/await on their code // class MainLoopSyncContext : SynchronizationContext { MainLoop mainLoop; public MainLoopSyncContext (MainLoop mainLoop) { this.mainLoop = mainLoop; } public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (MainLoop); } public override void Post (SendOrPostCallback d, object state) { mainLoop.AddIdle (() => { d (state); return false; }); //mainLoop.Driver.Wakeup (); } public override void Send (SendOrPostCallback d, object state) { mainLoop.Invoke (() => { d (state); }); } } /// /// If set, it forces the use of the System.Console-based driver. /// public static bool UseSystemConsole; /// /// Initializes a new instance of Application. /// /// /// /// Call this method once per instance (or after has been called). /// /// /// Loads the right for the platform. /// /// /// Creates a and assigns it to /// /// public static void Init (ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) => Init (() => Toplevel.Create (), driver, mainLoopDriver); internal static bool _initialized = false; /// /// Initializes the Terminal.Gui application /// static void Init (Func topLevelFactory, ConsoleDriver driver = null, IMainLoopDriver mainLoopDriver = null) { if (_initialized && driver == null) return; // Used only for start debugging on Unix. //#if DEBUG // while (!System.Diagnostics.Debugger.IsAttached) { // System.Threading.Thread.Sleep (100); // } // System.Diagnostics.Debugger.Break (); //#endif // Reset all class variables (Application is a singleton). ResetState (); // This supports Unit Tests and the passing of a mock driver/loopdriver if (driver != null) { if (mainLoopDriver == null) { throw new ArgumentNullException ("mainLoopDriver cannot be null if driver is provided."); } Driver = driver; Driver.Init (TerminalResized); MainLoop = new MainLoop (mainLoopDriver); SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } if (Driver == null) { var p = Environment.OSVersion.Platform; if (UseSystemConsole) { Driver = new NetDriver (); mainLoopDriver = new NetMainLoop (Driver); } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { Driver = new WindowsDriver (); mainLoopDriver = new WindowsMainLoop (Driver); } else { mainLoopDriver = new UnixMainLoop (); Driver = new CursesDriver (); } Driver.Init (TerminalResized); MainLoop = new MainLoop (mainLoopDriver); SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); } Top = topLevelFactory (); Current = Top; _initialized = true; } /// /// Captures the execution state for the provided view. /// public class RunState : IDisposable { /// /// Initializes a new class. /// /// public RunState (Toplevel view) { Toplevel = view; } internal Toplevel Toplevel; /// /// Releases alTop = l resource used by the object. /// /// Call when you are finished using the . The /// method leaves the in an unusable state. After /// calling , you must release all references to the /// so the garbage collector can reclaim the memory that the /// was occupying. public void Dispose () { Dispose (true); GC.SuppressFinalize (this); } /// /// Dispose the specified disposing. /// /// The dispose. /// If set to true disposing. protected virtual void Dispose (bool disposing) { if (Toplevel != null && disposing) { End (Toplevel); Toplevel.Dispose (); Toplevel = null; } } } static void ProcessKeyEvent (KeyEvent ke) { var chain = toplevels.ToList (); foreach (var topLevel in chain) { if (topLevel.ProcessHotKey (ke)) return; if (topLevel.Modal) break; } foreach (var topLevel in chain) { if (topLevel.ProcessKey (ke)) return; if (topLevel.Modal) break; } foreach (var topLevel in chain) { // Process the key normally if (topLevel.ProcessColdKey (ke)) return; if (topLevel.Modal) break; } } static void ProcessKeyDownEvent (KeyEvent ke) { var chain = toplevels.ToList (); foreach (var topLevel in chain) { if (topLevel.OnKeyDown (ke)) return; if (topLevel.Modal) break; } } static void ProcessKeyUpEvent (KeyEvent ke) { var chain = toplevels.ToList (); foreach (var topLevel in chain) { if (topLevel.OnKeyUp (ke)) return; if (topLevel.Modal) break; } } static View FindDeepestView (View start, int x, int y, out int resx, out int resy) { var startFrame = start.Frame; if (!startFrame.Contains (x, y)) { resx = 0; resy = 0; return null; } if (start.InternalSubviews != null) { int count = start.InternalSubviews.Count; if (count > 0) { var rx = x - startFrame.X; var ry = y - startFrame.Y; for (int i = count - 1; i >= 0; i--) { View v = start.InternalSubviews [i]; if (v.Visible && v.Frame.Contains (rx, ry)) { var deep = FindDeepestView (v, rx, ry, out resx, out resy); if (deep == null) return v; return deep; } } } } resx = x - startFrame.X; resy = y - startFrame.Y; return start; } internal static View mouseGrabView; /// /// Grabs the mouse, forcing all mouse events to be routed to the specified view until UngrabMouse is called. /// /// The grab. /// View that will receive all mouse events until UngrabMouse is invoked. public static void GrabMouse (View view) { if (view == null) return; mouseGrabView = view; Driver.UncookMouse (); } /// /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. /// public static void UngrabMouse () { mouseGrabView = null; Driver.CookMouse (); } /// /// Merely a debugging aid to see the raw mouse events /// public static Action RootMouseEvent; internal static View wantContinuousButtonPressedView; static View lastMouseOwnerView; static void ProcessMouseEvent (MouseEvent me) { var view = FindDeepestView (Current, me.X, me.Y, out int rx, out int ry); if (view != null && view.WantContinuousButtonPressed) wantContinuousButtonPressedView = view; else wantContinuousButtonPressedView = null; RootMouseEvent?.Invoke (me); if (mouseGrabView != null) { var newxy = mouseGrabView.ScreenToView (me.X, me.Y); var nme = new MouseEvent () { X = newxy.X, Y = newxy.Y, Flags = me.Flags, OfX = me.X - newxy.X, OfY = me.Y - newxy.Y, View = view }; if (OutsideFrame (new Point (nme.X, nme.Y), mouseGrabView.Frame)) { lastMouseOwnerView?.OnMouseLeave (me); } if (mouseGrabView != null) { mouseGrabView.OnMouseEvent (nme); return; } } if (view != null) { var nme = new MouseEvent () { X = rx, Y = ry, Flags = me.Flags, OfX = 0, OfY = 0, View = view }; if (lastMouseOwnerView == null) { lastMouseOwnerView = view; view.OnMouseEnter (nme); } else if (lastMouseOwnerView != view) { lastMouseOwnerView.OnMouseLeave (nme); view.OnMouseEnter (nme); lastMouseOwnerView = view; } if (!view.WantMousePositionReports && me.Flags == MouseFlags.ReportMousePosition) return; if (view.WantContinuousButtonPressed) wantContinuousButtonPressedView = view; else wantContinuousButtonPressedView = null; // Should we bubbled up the event, if it is not handled? view.OnMouseEvent (nme); } } static bool OutsideFrame (Point p, Rect r) { return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1; } /// /// Building block API: Prepares the provided for execution. /// /// The runstate handle that needs to be passed to the method upon completion. /// Toplevel to prepare execution for. /// /// This method prepares the provided toplevel for running with the focus, /// it adds this to the list of toplevels, sets up the mainloop to process the /// event, lays out the subviews, focuses the first element, and draws the /// toplevel in the screen. This is usually followed by executing /// the method, and then the method upon termination which will /// undo these changes. /// public static RunState Begin (Toplevel toplevel) { if (toplevel == null) throw new ArgumentNullException (nameof (toplevel)); var rs = new RunState (toplevel); Init (); if (toplevel is ISupportInitializeNotification initializableNotification && !initializableNotification.IsInitialized) { initializableNotification.BeginInit (); initializableNotification.EndInit (); } else if (toplevel is ISupportInitialize initializable) { initializable.BeginInit (); initializable.EndInit (); } toplevels.Push (toplevel); Current = toplevel; Driver.PrepareToRun (MainLoop, ProcessKeyEvent, ProcessKeyDownEvent, ProcessKeyUpEvent, ProcessMouseEvent); if (toplevel.LayoutStyle == LayoutStyle.Computed) toplevel.SetRelativeLayout (new Rect (0, 0, Driver.Cols, Driver.Rows)); toplevel.LayoutSubviews (); toplevel.WillPresent (); toplevel.OnLoaded (); Redraw (toplevel); toplevel.PositionCursor (); Driver.Refresh (); return rs; } /// /// Building block API: completes the execution of a that was started with . /// /// The runstate returned by the method. public static void End (RunState runState) { if (runState == null) throw new ArgumentNullException (nameof (runState)); runState.Toplevel.OnUnloaded (); runState.Dispose (); } /// /// Shutdown an application initialized with /// public static void Shutdown () { ResetState (); } // Encapsulate all setting of initial state for Application; Having // this in a function like this ensures we don't make mistakes in // guranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. static void ResetState () { // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 // TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520) foreach (var t in toplevels) { t.Running = false; t.Dispose (); } toplevels.Clear (); Current = null; Top = null; MainLoop = null; Driver?.End (); Driver = null; Iteration = null; RootMouseEvent = null; Resized = null; _initialized = false; // Reset synchronization context to allow the user to run async/await, // as the main loop has been ended, the synchronization context from // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/migueldeicaza/gui.cs/issues/1084). SynchronizationContext.SetSynchronizationContext (syncContext: null); } static void Redraw (View view) { view.Redraw (view.Bounds); Driver.Refresh (); } static void Refresh (View view) { view.Redraw (view.Bounds); Driver.Refresh (); } /// /// Triggers a refresh of the entire display. /// public static void Refresh () { Driver.UpdateScreen (); View last = null; foreach (var v in toplevels.Reverse ()) { v.SetNeedsDisplay (); v.Redraw (v.Bounds); last = v; } 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) { Current = null; } else { Current = toplevels.Peek (); Refresh (); } } /// /// Building block API: Runs the main loop for the created dialog /// /// /// Use the wait parameter to control whether this is a /// blocking or non-blocking call. /// /// The state returned by the Begin method. /// By default this is true which will execute the runloop waiting for events, if you pass false, you can use this method to run a single iteration of the events. 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"); bool firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel.Running;) { if (MainLoop.EventsPending (wait)) { // Notify Toplevel it's ready if (firstIteration) { state.Toplevel.OnReady (); } firstIteration = false; MainLoop.MainIteration (); Iteration?.Invoke (); if (Driver.EnsureCursorVisibility ()) { state.Toplevel.SetNeedsDisplay (); } } else if (!wait) { return; } if (state.Toplevel != Top && (!Top.NeedDisplay.IsEmpty || Top.ChildNeedsDisplay || Top.LayoutNeeded)) { Top.Redraw (Top.Bounds); state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds); } if (!state.Toplevel.NeedDisplay.IsEmpty || state.Toplevel.ChildNeedsDisplay || state.Toplevel.LayoutNeeded) { state.Toplevel.Redraw (state.Toplevel.Bounds); if (DebugDrawBounds) { DrawBounds (state.Toplevel); } state.Toplevel.PositionCursor (); Driver.Refresh (); } else { Driver.UpdateCursor (); } } } internal static bool DebugDrawBounds = false; // Need to look into why this does not work properly. static void DrawBounds (View v) { v.DrawFrame (v.Frame, padding: 0, fill: false); if (v.InternalSubviews != null && v.InternalSubviews.Count > 0) foreach (var sub in v.InternalSubviews) DrawBounds (sub); } /// /// Runs the application by calling with the value of /// public static void Run (Func errorHandler = null) { Run (Top, errorHandler); } /// /// Runs the application by calling with a new instance of the specified -derived class /// public static void Run (Func errorHandler = null) where T : Toplevel, new() { Init (() => new T ()); Run (Top, errorHandler); } /// /// 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 other modal s such as boxes. /// /// /// To make a stop execution, call . /// /// /// Calling is equivalent to calling , followed by , /// and then calling . /// /// /// Alternatively, to have a program control the main loop and /// process events manually, call to set things up manually and then /// repeatedly call with the wait parameter set to false. By doing this /// the method will only process any pending events, timers, idle handlers and /// then return control immediately. /// /// /// When is null the exception is rethrown, when it returns true the application is resumed and when false method exits gracefully. /// /// /// The tu run modally. /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). public static void Run (Toplevel view, Func errorHandler = null) { var resume = true; while (resume) { #if !DEBUG try { #endif resume = false; var runToken = Begin (view); RunLoop (runToken); End (runToken); #if !DEBUG } catch (Exception error) { if (errorHandler == null) { throw; } resume = errorHandler(error); } #endif } } /// /// Stops running the most recent . /// /// /// /// This will cause to return. /// /// /// Calling is equivalent to setting the property on the curently running to false. /// /// public static void RequestStop () { Current.Running = false; } /// /// Event arguments for the event. /// public class ResizedEventArgs : EventArgs { /// /// The number of rows in the resized terminal. /// public int Rows { get; set; } /// /// The number of columns in the resized terminal. /// public int Cols { get; set; } } /// /// Invoked when the terminal was resized. The new size of the terminal is provided. /// public static Action Resized; static void TerminalResized () { var full = new Rect (0, 0, Driver.Cols, Driver.Rows); Top.Frame = full; Top.Width = full.Width; Top.Height = full.Height; Resized?.Invoke (new ResizedEventArgs () { Cols = full.Width, Rows = full.Height }); Driver.Clip = full; foreach (var t in toplevels) { t.PositionToplevels (); t.SetRelativeLayout (full); t.LayoutSubviews (); } Refresh (); } } }