using System; using System.Collections.Generic; using System.Linq; using Terminal.Gui.Graphs; namespace Terminal.Gui { /// /// A consisting of a moveable bar that divides /// the display area into resizeable . /// public class TileView : View { TileView parentTileView; /// /// Use this field instead of Border to create an integrated /// Border in which lines connect with subviews and splitters /// seamlessly /// public BorderStyle IntegratedBorder { get; set; } /// /// A single presented in a . To create /// new instances use /// or . /// public class Tile { /// /// The that is showing in this . /// You should add new child views to this member if you want multiple /// within the . /// public View View { get; internal set; } /// /// Gets or Sets the minimum size you to allow when splitter resizing along /// parent direction. /// public int MinSize { get; set; } /// /// The text that should be displayed above the . This /// will appear over the splitter line or border (above the view client area). /// /// /// Title are not rendered for root level tiles if there is no /// to render into. /// public string Title { get; set; } /// /// Creates a new instance of the class. /// internal Tile () { View = new View () { Width = Dim.Fill (), Height = Dim.Fill () }; Title = string.Empty; MinSize = 0; } } List tiles; private List splitterDistances; private List splitterLines; /// /// The sub sections hosted by the view /// public IReadOnlyCollection Tiles => tiles.AsReadOnly (); /// /// The splitter locations. Note that there will be N-1 splitters where /// N is the number of . /// public IReadOnlyCollection SplitterDistances => splitterDistances.AsReadOnly (); private Orientation orientation = Orientation.Vertical; /// /// Creates a new instance of the class with /// 2 tiles (i.e. left and right). /// public TileView () : this (2) { } /// /// Creates a new instance of the class with /// number of tiles. /// /// public TileView (int tiles) { CanFocus = true; RebuildForTileCount (tiles); } /// /// Invoked when any of the is changed. /// public event SplitterEventHandler SplitterMoved; /// /// Raises the event /// protected virtual void OnSplitterMoved (int idx) { SplitterMoved?.Invoke (this, new SplitterEventArgs (this, idx, splitterDistances [idx])); } /// /// Scraps all and creates new tiles /// in orientation /// /// public void RebuildForTileCount (int count) { tiles = new List (); splitterDistances = new List (); splitterLines = new List (); RemoveAll (); tiles.Clear (); splitterDistances.Clear (); if (count == 0) { return; } for (int i = 0; i < count; i++) { if (i > 0) { var currentPos = Pos.Percent ((100 / count) * i); splitterDistances.Add (currentPos); var line = new TileViewLineView (this, i - 1); Add (line); splitterLines.Add (line); } var tile = new Tile (); tiles.Add (tile); Add (tile.View); } LayoutSubviews (); } /// /// Adds a new to the collection at . /// This will also add another splitter line /// /// /// public Tile InsertTile (int idx) { var oldTiles = Tiles.ToArray (); RebuildForTileCount (oldTiles.Length + 1); Tile toReturn = null; for(int i=0;i idx ? i - 1 : i]; // remove the new empty View Remove (tiles [i].View); // restore old Tile and View tiles [i] = oldTile; Add (tiles [i].View); } else { toReturn = tiles[i]; } } SetNeedsDisplay (); LayoutSubviews (); return toReturn; } /// /// Removes a at the provided from /// the view. Returns the removed tile or null if already empty. /// /// /// public Tile RemoveTile (int idx) { var oldTiles = Tiles.ToArray (); if (idx < 0 || idx >= oldTiles.Length) { return null; } var removed = Tiles.ElementAt (idx); RebuildForTileCount (oldTiles.Length - 1); for (int i = 0; i < tiles.Count; i++) { int oldIdx = i >= idx ? i + 1: i; var oldTile = oldTiles [oldIdx]; // remove the new empty View Remove (tiles [i].View); // restore old Tile and View tiles [i] = oldTile; Add (tiles [i].View); } SetNeedsDisplay (); LayoutSubviews (); return removed; } /// /// Returns the index of the first in /// which contains . /// public int IndexOf(View toFind, bool recursive = false) { for(int i = 0 ;i < tiles.Count; i++) { var v = tiles[i].View; if(v == toFind) { return i; } if(v.Subviews.Contains(toFind)) { return i; } if(recursive) { if(RecursiveContains(v.Subviews,toFind)) { return i; } } } return -1; } private bool RecursiveContains (IEnumerable haystack, View needle) { foreach(var v in haystack) { if(v == needle) { return true; } if(RecursiveContains(v.Subviews,needle)) { return true; } } return false; } /// /// Orientation of the dividing line (Horizontal or Vertical). /// public Orientation Orientation { get { return orientation; } set { orientation = value; LayoutSubviews (); } } /// public override void LayoutSubviews () { var contentArea = Bounds; if (HasBorder ()) { // TODO: Bound with Max/Min contentArea = new Rect ( contentArea.X + 1, contentArea.Y + 1, Math.Max (0, contentArea.Width - 2), Math.Max (0, contentArea.Height - 2)); } Setup (contentArea); base.LayoutSubviews (); } /// /// Distance Horizontally or Vertically to the splitter line when /// neither view is collapsed. /// /// Only absolute values (e.g. 10) and percent values (i.e. ) /// are supported for this property. /// public void SetSplitterPos (int idx, Pos value) { if (!(value is Pos.PosAbsolute) && !(value is Pos.PosFactor)) { throw new ArgumentException ($"Only Percent and Absolute values are supported. Passed value was {value.GetType ().Name}"); } splitterDistances [idx] = value; GetRootTileView ().LayoutSubviews (); OnSplitterMoved (idx); } /// public override bool OnEnter (View view) { Driver.SetCursorVisibility (CursorVisibility.Invisible); return base.OnEnter (view); } /// public override void Redraw (Rect bounds) { // TODO: We are getting passed stale bounds, does this only happen in TileView // or is it a larger problem? This line should not be required right? bounds = Bounds; Driver.SetAttribute (ColorScheme.Normal); Clear (); base.Redraw (bounds); var lc = new LineCanvas (); var allLines = GetAllLineViewsRecursively (this); var allTitlesToRender = GetAllTitlesToRenderRecursively(this); if (IsRootTileView ()) { if (HasBorder ()) { lc.AddLine (new Point (0, 0), bounds.Width - 1, Orientation.Horizontal, IntegratedBorder); lc.AddLine (new Point (0, 0), bounds.Height - 1, Orientation.Vertical, IntegratedBorder); lc.AddLine (new Point (bounds.Width - 1, bounds.Height - 1), -bounds.Width + 1, Orientation.Horizontal, IntegratedBorder); lc.AddLine (new Point (bounds.Width - 1, bounds.Height - 1), -bounds.Height + 1, Orientation.Vertical, IntegratedBorder); } foreach (var line in allLines) { bool isRoot = splitterLines.Contains (line); line.ViewToScreen (0, 0, out var x1, out var y1); var origin = ScreenToView (x1, y1); var length = line.Orientation == Orientation.Horizontal ? line.Frame.Width - 1 : line.Frame.Height - 1; if (!isRoot) { if (line.Orientation == Orientation.Horizontal) { origin.X -= 1; } else { origin.Y -= 1; } length += 2; } lc.AddLine (origin, length, line.Orientation, IntegratedBorder); } } Driver.SetAttribute (ColorScheme.Normal); lc.Draw (this, bounds); // Redraw the lines so that focus/drag symbol renders foreach (var line in allLines) { line.DrawSplitterSymbol (); } // Draw Titles over Border foreach(var titleToRender in allTitlesToRender) { var renderAt = titleToRender.GetLocalCoordinateForTitle(this); if(renderAt.Y < 0) { // If we have no border then root level tiles // have nowhere to render their titles. continue; } // TODO: Render with focus color if focused var title = titleToRender.Tile.Title; for(int i=0;i /// Converts of element /// from a regular to a new nested with /// number of panels. Returns false if the element already contains a nested view. /// /// After successful splitting, the /// will contain the previous (replaced) at element 0. /// The element of that is to be subdivided. /// The number of panels that the should be split into /// The new nested . /// if a was converted to a new nested /// . if it was already a nested /// public bool TrySplitTile(int idx, int panels, out TileView result) { // when splitting a view into 2 sub views we will need to migrate // the title too var tile = tiles [idx]; // TODO: migrate the title too right? var title = tile.Title; View toMove = tile.View; if (toMove is TileView existing) { result = existing; return false; } var newContainer = new TileView(panels) { Width = Dim.Fill (), Height = Dim.Fill (), parentTileView = this, }; // Take everything out of the View we are moving var childViews = toMove.Subviews.ToArray(); toMove.RemoveAll (); // Remove the view itself and replace it with the new TileView Remove (toMove); Add (newContainer); tile.View = newContainer; var newTileView1 = newContainer.tiles [0].View; // Add the original content into the first view of the new container foreach (var childView in childViews) { newTileView1.Add (childView); } result = newContainer; return true; } private List GetAllLineViewsRecursively (View v) { var lines = new List (); foreach (var sub in v.Subviews) { if (sub is TileViewLineView s) { if (s.Visible && s.Parent.GetRootTileView () == this) { lines.Add (s); } } else { if(sub.Visible) { lines.AddRange (GetAllLineViewsRecursively (sub)); } } } return lines; } private List GetAllTitlesToRenderRecursively (TileView v, int depth = 0) { var titles = new List (); foreach (var sub in v.Tiles) { // Don't render titles for invisible stuff! if(!sub.View.Visible) { continue; } if(sub.View is TileView subTileView) { // Panels with sub split tiles in them can never // have their Titles rendered. Instead we dive in // and pull up their children as titles titles.AddRange (GetAllTitlesToRenderRecursively (subTileView,depth+1)); } else { if(sub.Title.Length > 0) { titles.Add(new TileTitleToRender(sub,depth)); } } } return titles; } /// /// /// if is nested within a parent /// e.g. via the . if it is a root level . /// /// /// Note that manually adding one to another will not result in a parent/child /// relationship and both will still be considered 'root' containers. Always use /// if you want to subdivide a . /// public bool IsRootTileView () { // TODO: don't want to layout subviews since the parent recursively lays them all out return parentTileView == null; } /// /// Returns the immediate parent of this. Note that in case /// of deep nesting this might not be the root . Returns null /// if this instance is not a nested child (created with /// ) /// /// /// Use to determine if the returned value is the root. /// /// public TileView GetParentTileView () { return this.parentTileView; } private TileView GetRootTileView () { TileView root = this; while (root.parentTileView != null) { root = root.parentTileView; } return root; } private void Setup (Rect bounds) { if (bounds.IsEmpty || bounds.Height <= 0 || bounds.Width <= 0) { return; } RespectMinimumTileSizes (); for (int i = 0; i < splitterLines.Count; i++) { var line = splitterLines[i]; line.Orientation = Orientation; line.Width = orientation == Orientation.Vertical ? 1 : Dim.Fill (); line.Height = orientation == Orientation.Vertical ? Dim.Fill () : 1; line.LineRune = orientation == Orientation.Vertical ? Driver.VLine : Driver.HLine; if (orientation == Orientation.Vertical) { line.X = splitterDistances [i]; line.Y = 0; } else { line.Y = splitterDistances [i]; line.X = 0; } } HideSplittersBasedOnTileVisibility (); var visibleTiles = tiles.Where (t => t.View.Visible).ToArray (); var visibleSplitterLines = splitterLines.Where (l => l.Visible).ToArray (); for (int i = 0; i < visibleTiles.Length; i++) { var tile = visibleTiles [i]; // TODO: Deal with lines being Visibility false if (Orientation == Orientation.Vertical) { tile.View.X = i == 0 ? bounds.X : Pos.Right (visibleSplitterLines [i - 1]); tile.View.Y = bounds.Y; tile.View.Height = bounds.Height; tile.View.Width = GetTileWidthOrHeight(i, Bounds.Width, visibleTiles,visibleSplitterLines); } else { tile.View.X = bounds.X; tile.View.Y = i == 0 ? 0 : Pos.Bottom (visibleSplitterLines [i - 1]); tile.View.Width = bounds.Width; tile.View.Height = GetTileWidthOrHeight(i, Bounds.Height, visibleTiles, visibleSplitterLines); } } } private void HideSplittersBasedOnTileVisibility () { if(splitterLines.Count == 0) { return; } foreach(var line in splitterLines) { line.Visible = true; } for(int i=0;i