Merge branch 'v2_develop' into copilot/fix-mouse-event-routing-issue

This commit is contained in:
Tig
2026-02-06 14:43:21 -07:00
committed by GitHub
17 changed files with 946 additions and 491 deletions

View File

@@ -1,7 +1,6 @@
using System;
#nullable enable
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
@@ -12,49 +11,49 @@ namespace UICatalog.Scenarios;
[ScenarioCategory ("Arrangement")]
public class DynamicStatusBar : Scenario
{
private static IApplication? _app;
public override void Main ()
{
ConfigurationManager.Enable (ConfigLocations.All);
using IApplication app = Application.Create ();
app.Init ();
app.Run<DynamicStatusBarSample> ();
_app = app;
_app.Init ();
_app.Run<DynamicStatusBarSample> ();
}
public class Binding
{
private readonly PropertyInfo _sourceBindingProperty;
private readonly object _sourceDataContext;
private readonly IValueConverter _valueConverter;
private readonly PropertyInfo? _sourceBindingProperty;
private readonly object? _sourceDataContext;
private readonly IValueConverter? _valueConverter;
public Binding (
View source,
string sourcePropertyName,
View target,
string targetPropertyName,
IValueConverter valueConverter = null
IValueConverter? valueConverter = null
)
{
Target = target;
Source = source;
SourcePropertyName = sourcePropertyName;
TargetPropertyName = targetPropertyName;
_sourceDataContext = Source.GetType ().GetProperty ("DataContext").GetValue (Source);
_sourceBindingProperty = _sourceDataContext.GetType ().GetProperty (SourcePropertyName);
_sourceDataContext = Source.GetType ().GetProperty ("DataContext")?.GetValue (Source);
_sourceBindingProperty = _sourceDataContext?.GetType ().GetProperty (SourcePropertyName);
_valueConverter = valueConverter;
UpdateTarget ();
var notifier = (INotifyPropertyChanged)_sourceDataContext;
var notifier = (INotifyPropertyChanged)_sourceDataContext!;
if (notifier != null)
{
notifier.PropertyChanged += (s, e) =>
notifier.PropertyChanged += (_, e) =>
{
if (e.PropertyName == SourcePropertyName)
{
if (e.PropertyName == SourcePropertyName)
{
UpdateTarget ();
}
};
}
UpdateTarget ();
}
};
}
public View Source { get; }
@@ -66,7 +65,7 @@ public class DynamicStatusBar : Scenario
{
try
{
object sourceValue = _sourceBindingProperty.GetValue (_sourceDataContext);
object? sourceValue = _sourceBindingProperty?.GetValue (_sourceDataContext);
if (sourceValue == null)
{
@@ -75,8 +74,8 @@ public class DynamicStatusBar : Scenario
object finalValue = _valueConverter?.Convert (sourceValue) ?? sourceValue;
PropertyInfo targetProperty = Target.GetType ().GetProperty (TargetPropertyName);
targetProperty.SetValue (Target, finalValue);
PropertyInfo? targetProperty = Target.GetType ().GetProperty (TargetPropertyName);
targetProperty?.SetValue (Target, finalValue);
}
catch (Exception ex)
{
@@ -87,9 +86,9 @@ public class DynamicStatusBar : Scenario
public class DynamicStatusBarDetails : FrameView
{
private Shortcut _statusItem;
private Shortcut? _statusItem;
public DynamicStatusBarDetails (Shortcut statusItem = null) : this ()
public DynamicStatusBarDetails (Shortcut? statusItem = null) : this ()
{
_statusItem = statusItem;
Title = statusItem == null ? "Adding New StatusBar Item." : "Editing StatusBar Item.";
@@ -98,52 +97,55 @@ public class DynamicStatusBar : Scenario
public DynamicStatusBarDetails ()
{
var lblTitle = new Label { Y = 1, Text = "Title:" };
base.Add (lblTitle);
Add (lblTitle);
TextTitle = new TextField { X = Pos.Right (lblTitle) + 4, Y = Pos.Top (lblTitle), Width = Dim.Fill () };
base.Add (TextTitle);
Add (TextTitle);
var lblAction = new Label { X = Pos.Left (lblTitle), Y = Pos.Bottom (lblTitle) + 1, Text = "Action:" };
base.Add (lblAction);
Add (lblAction);
TextAction = new TextView
{
X = Pos.Left (TextTitle), Y = Pos.Top (lblAction), Width = Dim.Fill (), Height = 5
};
base.Add (TextAction);
Add (TextAction);
var lblShortcut = new Label
var lblKey = new Label
{
X = Pos.Left (lblTitle), Y = Pos.Bottom (TextAction) + 1, Text = "Shortcut:"
X = Pos.Left (lblTitle), Y = Pos.Bottom (TextAction) + 1, Text = "Key:"
};
base.Add (lblShortcut);
Add (lblKey);
TextShortcut = new TextField
TextKey = new TextField
{
X = Pos.X (TextAction), Y = Pos.Y (lblShortcut), Width = Dim.Fill (), ReadOnly = true
X = Pos.X (TextAction), Y = Pos.Y (lblKey), Width = Dim.Fill (), ReadOnly = true
};
TextShortcut.KeyDown += (s, e) =>
TextKey.KeyDown += (_, e) =>
{
TextShortcut.Text = e.ToString ();
TextKey.Text = e.ToString ();
};
base.Add (TextShortcut);
Add (TextKey);
var btnShortcut = new Button
var btnKey = new Button
{
X = Pos.X (lblShortcut), Y = Pos.Bottom (TextShortcut) + 1, Text = "Clear Shortcut"
X = Pos.X (lblKey), Y = Pos.Bottom (TextKey) + 1, Text = "Clear Key"
};
btnShortcut.Accepting += (s, e) => { TextShortcut.Text = ""; };
base.Add (btnShortcut);
btnKey.Accepting += (_, e) =>
{
TextKey.Text = "";
e.Handled = true;
};
Add (btnKey);
}
public TextView TextAction { get; }
public TextField TextShortcut { get; }
public TextField TextKey { get; }
public TextField TextTitle { get; }
public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (App!, item.Title, item.Action, "Ok"); }
public Action CreateAction (DynamicStatusItem item) => () => MessageBox.ErrorQuery (_app!, item.Title, item.Action, "Ok");
public void EditStatusItem (Shortcut statusItem)
public void EditStatusItem (Shortcut? statusItem)
{
if (statusItem == null)
{
@@ -155,16 +157,16 @@ public class DynamicStatusBar : Scenario
Enabled = true;
_statusItem = statusItem;
TextTitle.Text = statusItem?.Title ?? "";
TextTitle.Text = statusItem.Title;
TextAction.Text = statusItem != null && statusItem.Action != null
TextAction.Text = statusItem.Action != null
? GetTargetAction (statusItem.Action)
: string.Empty;
TextShortcut.Text = statusItem.Key;
TextKey.Text = statusItem.Key == Key.Empty ? "" : statusItem.Key;
}
public DynamicStatusItem EnterStatusItem ()
public DynamicStatusItem? EnterStatusItem ()
{
var valid = false;
@@ -181,41 +183,40 @@ public class DynamicStatusBar : Scenario
var btnOk = new Button { IsDefault = true, Text = "OK" };
Dialog dialog = new () { Title = "Enter the menu details." };
btnOk.Accepting += (s, e) =>
btnOk.Accepting += (_, _) =>
{
if (string.IsNullOrEmpty (TextTitle.Text))
{
MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok");
MessageBox.ErrorQuery (_app!, "Invalid title", "Must enter a valid title!.", "Ok");
}
else
{
valid = true;
dialog.App?.RequestStop ();
_app?.RequestStop ();
}
};
var btnCancel = new Button { Text = "Cancel" };
btnCancel.Accepting += (s, e) =>
btnCancel.Accepting += (_, _) =>
{
TextTitle.Text = string.Empty;
dialog.App?.RequestStop ();
_app?.RequestStop ();
};
dialog.Buttons = [btnOk, btnCancel];
Width = Dim.Fill (0, minimumContentDim: 50);
Height = Dim.Fill (0, minimumContentDim: 10) - 2;
Dialog dialog = new () { Title = "Enter the Shortcut details.", Buttons = [btnOk, btnCancel] };
Width = Dim.Auto ();
Height = Dim.Auto ();
dialog.Add (this);
TextTitle.SetFocus ();
TextTitle.InsertionPoint = TextTitle.Text.Length;
App?.Run (dialog);
_app?.Run (dialog);
dialog.Dispose ();
return valid
? new DynamicStatusItem
{
Title = TextTitle.Text, Action = TextAction.Text, Shortcut = TextShortcut.Text
Title = TextTitle.Text, Action = TextAction.Text, Key = TextKey.Text
}
: null;
}
@@ -224,12 +225,12 @@ public class DynamicStatusBar : Scenario
{
TextTitle.Text = "";
TextAction.Text = "";
TextShortcut.Text = "";
TextKey.Text = "";
}
private string GetTargetAction (Action action)
{
object me = action.Target;
object? me = action.Target;
if (me == null)
{
@@ -246,17 +247,16 @@ public class DynamicStatusBar : Scenario
}
}
return v == null || !(v is DynamicStatusItem item) ? string.Empty : item.Action;
return v is not DynamicStatusItem item ? string.Empty : item.Action;
}
}
public class DynamicStatusBarSample : Window
{
private readonly ListView _lstItems;
private Shortcut _currentEditStatusItem;
private Shortcut? _currentEditStatusItem;
private int _currentSelectedStatusBar = -1;
private Shortcut _currentStatusItem;
private StatusBar _statusBar;
private StatusBar? _statusBar;
public DynamicStatusBarSample ()
{
@@ -312,7 +312,7 @@ public class DynamicStatusBar : Scenario
};
Add (frmStatusBarDetails);
btnUp.Accepting += (s, e) =>
btnUp.Accepting += (_, _) =>
{
if (_lstItems.SelectedItem is null)
{
@@ -320,27 +320,31 @@ public class DynamicStatusBar : Scenario
}
int i = _lstItems.SelectedItem.Value;
Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
Shortcut? statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
if (statusItem != null)
if (statusItem == null)
{
Shortcut [] items = _statusBar.SubViews.OfType<Shortcut> ().ToArray ();
if (i > 0)
{
items [i] = items [i - 1];
items [i - 1] = statusItem;
DataContext.Items [i] = DataContext.Items [i - 1];
DataContext.Items [i - 1] =
new DynamicStatusItemList (statusItem.Title, statusItem);
_lstItems.SelectedItem = i - 1;
_statusBar.SetNeedsDraw ();
}
return;
}
Shortcut [] items = _statusBar!.SubViews.OfType<Shortcut> ().ToArray ();
if (i <= 0)
{
return;
}
items [i] = items [i - 1];
items [i - 1] = statusItem;
DataContext.Items [i] = DataContext.Items [i - 1];
DataContext.Items [i - 1] =
new DynamicStatusItemList (statusItem.Title, statusItem);
_lstItems.SelectedItem = _currentSelectedStatusBar = i - 1;
Shortcut toMove = _statusBar.RemoveShortcut (i)!;
_statusBar.AddShortcutAt (i - 1, toMove);
_statusBar.SetNeedsLayout ();
};
btnDown.Accepting += (s, e) =>
btnDown.Accepting += (_, _) =>
{
if (_lstItems.SelectedItem is null)
{
@@ -348,24 +352,28 @@ public class DynamicStatusBar : Scenario
}
int i = _lstItems.SelectedItem.Value;
Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
Shortcut? statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null;
if (statusItem != null)
if (statusItem == null)
{
Shortcut [] items = _statusBar.SubViews.OfType<Shortcut> ().ToArray ();
if (i < items.Length - 1)
{
items [i] = items [i + 1];
items [i + 1] = statusItem;
DataContext.Items [i] = DataContext.Items [i + 1];
DataContext.Items [i + 1] =
new DynamicStatusItemList (statusItem.Title, statusItem);
_lstItems.SelectedItem = i + 1;
_statusBar.SetNeedsDraw ();
}
return;
}
Shortcut [] items = _statusBar!.SubViews.OfType<Shortcut> ().ToArray ();
if (i >= items.Length - 1)
{
return;
}
items [i] = items [i + 1];
items [i + 1] = statusItem;
DataContext.Items [i] = DataContext.Items [i + 1];
DataContext.Items [i + 1] =
new DynamicStatusItemList (statusItem.Title, statusItem);
_lstItems.SelectedItem = _currentSelectedStatusBar = i + 1;
Shortcut toMove = _statusBar.RemoveShortcut (i)!;
_statusBar.AddShortcutAt (i + 1, toMove);
_statusBar.SetNeedsLayout ();
};
var btnOk = new Button
@@ -375,16 +383,20 @@ public class DynamicStatusBar : Scenario
Add (btnOk);
var btnCancel = new Button { X = Pos.Right (btnOk) + 3, Y = Pos.Top (btnOk), Text = "Cancel" };
btnCancel.Accepting += (s, e) => { SetFrameDetails (_currentEditStatusItem); };
btnCancel.Accepting += (_, _) => { SetFrameDetails (_currentEditStatusItem); };
Add (btnCancel);
_lstItems.ValueChanged += (_, _) => { SetFrameDetails (); };
_lstItems.ValueChanged += (_, e) =>
{
_currentSelectedStatusBar = e.NewValue ?? -1;
SetFrameDetails ();
};
btnOk.Accepting += (s, e) =>
btnOk.Accepting += (_, _) =>
{
if (string.IsNullOrEmpty (frmStatusBarDetails.TextTitle.Text) && _currentEditStatusItem != null)
{
MessageBox.ErrorQuery (App, "Invalid title", "Must enter a valid title!.", "Ok");
MessageBox.ErrorQuery (_app!, "Invalid title", "Must enter a valid title!.", "Ok");
}
else if (_currentEditStatusItem != null)
{
@@ -392,7 +404,7 @@ public class DynamicStatusBar : Scenario
{
Title = frmStatusBarDetails.TextTitle.Text,
Action = frmStatusBarDetails.TextAction.Text,
Shortcut = frmStatusBarDetails.TextShortcut.Text
Key = frmStatusBarDetails.TextKey.Text
};
if (_lstItems.SelectedItem is { } selectedItem)
@@ -402,22 +414,21 @@ public class DynamicStatusBar : Scenario
}
};
btnAdd.Accepting += (s, e) =>
btnAdd.Accepting += (_, _) =>
{
//if (StatusBar == null)
//{
// MessageBox.ErrorQuery (
// "StatusBar Bar Error",
// "Must add a StatusBar first!",
// "Ok"
// );
// _btnAddStatusBar.SetFocus ();
if (_statusBar == null)
{
MessageBox.ErrorQuery (_app!,
"StatusBar Bar Error",
"Must add a StatusBar first!",
"Ok");
btnAddStatusBar.SetFocus ();
// return;
//}
return;
}
var frameDetails = new DynamicStatusBarDetails ();
DynamicStatusItem item = frameDetails.EnterStatusItem ();
DynamicStatusItem? item = frameDetails.EnterStatusItem ();
if (item == null)
{
@@ -432,37 +443,38 @@ public class DynamicStatusBar : Scenario
SetFrameDetails ();
};
btnRemove.Accepting += (s, e) =>
btnRemove.Accepting += (_, _) =>
{
Shortcut statusItem = DataContext.Items.Count > 0
? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
Shortcut? statusItem = DataContext.Items.Count > 0
? DataContext.Items [_lstItems.SelectedItem!.Value].Shortcut
: null;
if (statusItem != null)
if (statusItem == null)
{
_statusBar.RemoveShortcut (_currentSelectedStatusBar);
statusItem.Dispose ();
DataContext.Items.RemoveAt (_lstItems.SelectedItem.Value);
if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1)
{
_lstItems.SelectedItem = _lstItems.Source.Count - 1;
}
_lstItems.SetNeedsDraw ();
SetFrameDetails ();
return;
}
Shortcut? removed = _statusBar?.RemoveShortcut (_currentSelectedStatusBar);
removed?.Dispose ();
DataContext.Items.RemoveAt (_lstItems.SelectedItem!.Value);
if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1)
{
_lstItems.SelectedItem = _lstItems.Source.Count - 1;
}
_currentSelectedStatusBar = _lstItems.SelectedItem ?? -1;
_lstItems.SetNeedsDraw ();
SetFrameDetails ();
};
_lstItems.HasFocusChanging += (s, e) =>
_lstItems.HasFocusChanging += (_, _) =>
{
Shortcut statusItem = DataContext.Items.Count > 0
? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
: null;
Shortcut? statusItem = DataContext.Items.Count > 0
? DataContext.Items [_lstItems.SelectedItem!.Value].Shortcut
: null;
SetFrameDetails (statusItem);
};
btnAddStatusBar.Accepting += (s, e) =>
btnAddStatusBar.Accepting += (_, _) =>
{
if (_statusBar != null)
{
@@ -473,7 +485,7 @@ public class DynamicStatusBar : Scenario
Add (_statusBar);
};
btnRemoveStatusBar.Accepting += (s, e) =>
btnRemoveStatusBar.Accepting += (_, _) =>
{
if (_statusBar == null)
{
@@ -484,27 +496,26 @@ public class DynamicStatusBar : Scenario
_statusBar.Dispose ();
_statusBar = null;
DataContext.Items = [];
_currentStatusItem = null;
Shortcut? currentStatusItem1 = null;
_currentSelectedStatusBar = -1;
SetListViewSource (_currentStatusItem, true);
SetListViewSource (currentStatusItem1, true);
SetFrameDetails ();
};
SetFrameDetails ();
var ustringConverter = new UStringValueConverter ();
var listWrapperConverter = new ListWrapperConverter<DynamicStatusItemList> ();
_ = new Binding (this, "Items", _lstItems, "Source", new ListWrapperConverter<DynamicStatusItemList> ());
var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter);
return;
void SetFrameDetails (Shortcut statusItem = null)
void SetFrameDetails (Shortcut? statusItem = null)
{
Shortcut newStatusItem;
Shortcut? newStatusItem;
if (statusItem == null)
{
newStatusItem = DataContext.Items.Count > 0
? DataContext.Items [_lstItems.SelectedItem.Value].Shortcut
? DataContext.Items [_lstItems.SelectedItem!.Value].Shortcut
: null;
}
else
@@ -516,35 +527,37 @@ public class DynamicStatusBar : Scenario
frmStatusBarDetails.EditStatusItem (newStatusItem);
bool f = btnOk.Enabled == frmStatusBarDetails.Enabled;
if (!f)
if (f)
{
btnOk.Enabled = frmStatusBarDetails.Enabled;
btnCancel.Enabled = frmStatusBarDetails.Enabled;
return;
}
btnOk.Enabled = frmStatusBarDetails.Enabled;
btnCancel.Enabled = frmStatusBarDetails.Enabled;
}
void SetListViewSource (Shortcut currentStatusItem, bool fill = false)
void SetListViewSource (Shortcut? currentStatusItem, bool fill = false)
{
DataContext.Items = [];
Shortcut statusItem = currentStatusItem;
if (!fill)
{
return;
}
if (statusItem != null)
if (currentStatusItem == null)
{
foreach (Shortcut si in _statusBar.SubViews.OfType<Shortcut> ())
{
DataContext.Items.Add (new DynamicStatusItemList (si.Title, si));
}
return;
}
foreach (Shortcut si in _statusBar?.SubViews.OfType<Shortcut> ()!)
{
DataContext.Items.Add (new DynamicStatusItemList (si.Title, si));
}
}
Shortcut CreateNewStatusBar (DynamicStatusItem item)
{
var newStatusItem = new Shortcut (item.Shortcut, item.Title, frmStatusBarDetails.CreateAction (item));
var newStatusItem = new Shortcut (item.Key, item.Title, frmStatusBarDetails.CreateAction (item));
return newStatusItem;
}
@@ -555,24 +568,18 @@ public class DynamicStatusBar : Scenario
int index
)
{
_statusBar.SubViews.ElementAt (index).Title = statusItem.Title;
((Shortcut)_statusBar.SubViews.ElementAt (index)).Action = frmStatusBarDetails.CreateAction (statusItem);
((Shortcut)_statusBar.SubViews.ElementAt (index)).Key = statusItem.Shortcut;
_statusBar?.SubViews.ElementAt (index).Title = statusItem.Title;
((Shortcut)_statusBar?.SubViews.ElementAt (index)!).Action = frmStatusBarDetails.CreateAction (statusItem);
((Shortcut)_statusBar.SubViews.ElementAt (index)).Key = statusItem.Key;
if (DataContext.Items.Count == 0)
{
DataContext.Items.Add (
new DynamicStatusItemList (
currentEditStatusItem.Title,
currentEditStatusItem
)
);
DataContext.Items.Add (new DynamicStatusItemList (currentEditStatusItem.Title,
currentEditStatusItem));
}
DataContext.Items [index] = new DynamicStatusItemList (
currentEditStatusItem.Title,
currentEditStatusItem
);
DataContext.Items [index] = new DynamicStatusItemList (currentEditStatusItem.Title,
currentEditStatusItem);
SetFrameDetails (currentEditStatusItem);
}
@@ -580,95 +587,59 @@ public class DynamicStatusBar : Scenario
}
public DynamicStatusItemModel DataContext { get; set; }
}
public class DynamicStatusItem
{
public string Action { get; set; } = "";
public string Shortcut { get; set; }
public string Action { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public string Title { get; set; } = "New";
}
public class DynamicStatusItemList
public class DynamicStatusItemList (string title, Shortcut statusItem)
{
public DynamicStatusItemList () { }
public DynamicStatusItemList (string title, Shortcut statusItem)
{
Title = title;
Shortcut = statusItem;
}
public Shortcut Shortcut { get; set; }
public string Title { get; set; }
public override string ToString () { return $"{Title}, {Shortcut.Key}"; }
public Shortcut Shortcut { get; set; } = statusItem;
public string Title { get; set; } = title;
public override string ToString () => $"{Title}, {Shortcut.Key}";
}
public class DynamicStatusItemModel : INotifyPropertyChanged
{
private ObservableCollection<DynamicStatusItemList> _items;
private string _statusBar;
public DynamicStatusItemModel () { Items = []; }
public DynamicStatusItemModel () => Items = [];
public ObservableCollection<DynamicStatusItemList> Items
{
get => _items;
get;
set
{
if (value == _items)
if (value == field)
{
return;
}
_items = value;
field = value;
PropertyChanged?.Invoke (
this,
new PropertyChangedEventArgs (GetPropertyName ())
);
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ()));
}
}
public string StatusBar
{
get => _statusBar;
set
{
if (value == _statusBar)
{
return;
}
_statusBar = value;
PropertyChanged?.Invoke (
this,
new PropertyChangedEventArgs (GetPropertyName ())
);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public string GetPropertyName ([CallerMemberName] string propertyName = null) { return propertyName; }
public event PropertyChangedEventHandler? PropertyChanged;
public string? GetPropertyName ([CallerMemberName] string? propertyName = null) => propertyName;
}
public interface IValueConverter
{
object Convert (object value, object parameter = null);
object Convert (object value, object? parameter = null);
}
public class ListWrapperConverter<T> : IValueConverter
{
public object Convert (object value, object parameter = null) { return new ListWrapper<T> ((ObservableCollection<T>)value); }
public object Convert (object value, object? parameter = null) => new ListWrapper<T> ((ObservableCollection<T>)value);
}
public class UStringValueConverter : IValueConverter
{
public object Convert (object value, object parameter = null)
public object Convert (object value, object? parameter = null)
{
byte [] data = Encoding.ASCII.GetBytes (value.ToString () ?? string.Empty);

View File

@@ -485,6 +485,44 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
internal virtual int GetMinimumContribution (int location, int superviewContentSize, View us, Dimension dimension) =>
Calculate (location, superviewContentSize, us, dimension);
/// <summary>
/// Indicates whether this Dim has a fixed value that doesn't depend on layout calculations.
/// </summary>
/// <remarks>
/// <para>
/// This property is used by <see cref="DimAuto"/> to identify dimensions that can be
/// determined without performing layout calculations on other views.
/// </para>
/// <para>
/// Fixed dimensions include <see cref="DimAbsolute"/> and dimensions calculated by
/// <see cref="DimFunc"/> that don't depend on other views' layouts.
/// </para>
/// </remarks>
/// <returns>
/// <see langword="true"/> if this Dim has a fixed value independent of layout;
/// otherwise, <see langword="false"/>.
/// </returns>
internal virtual bool IsFixed => false;
/// <summary>
/// Indicates whether this Dim requires the target view to be laid out before it can be calculated.
/// </summary>
/// <remarks>
/// <para>
/// This property is used by <see cref="DimAuto"/> to identify dimensions that depend on
/// another view's layout being completed first.
/// </para>
/// <para>
/// Dimensions that require target layout include <see cref="DimView"/> which depends on
/// the target view's calculated size.
/// </para>
/// </remarks>
/// <returns>
/// <see langword="true"/> if this Dim requires the target view's layout to be calculated first;
/// otherwise, <see langword="false"/>.
/// </returns>
internal virtual bool RequiresTargetLayout => false;
#endregion virtual methods
#region operators

View File

@@ -23,4 +23,7 @@ public record DimAbsolute (int Size) : Dim
internal override int GetAnchor (int size) => Size;
internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) => Math.Max (GetAnchor (0), 0);
/// <inheritdoc/>
internal override bool IsFixed => true;
}

View File

@@ -35,6 +35,129 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
/// <inheritdoc/>
internal override int GetAnchor (int size) => 0;
/// <inheritdoc/>
internal override bool IsFixed => true;
/// <summary>
/// Holds categorized views for single-pass processing.
/// Phase 1 and 2 Performance Optimization: Reduces iterations and allocations.
/// </summary>
private readonly struct ViewCategories
{
public List<View> NotDependent { get; init; }
public List<View> Centered { get; init; }
public List<View> Anchored { get; init; }
public List<View> PosViewBased { get; init; }
public List<View> DimViewBased { get; init; }
public List<View> DimAutoBased { get; init; }
public List<View> DimFillBased { get; init; }
public List<int> AlignGroupIds { get; init; }
}
/// <summary>
/// Categorizes views in a single pass to reduce iterations and allocations.
/// Phase 1 and 2 Performance Optimization.
/// </summary>
private static ViewCategories CategorizeViews (IList<View> subViews, Dimension dimension, int superviewContentSize)
{
ViewCategories categories = new ()
{
NotDependent = [],
Centered = [],
Anchored = [],
PosViewBased = [],
DimViewBased = [],
DimAutoBased = [],
DimFillBased = [],
AlignGroupIds = []
};
HashSet<int> seenAlignGroupIds = new ();
foreach (View v in subViews)
{
Pos pos = dimension == Dimension.Width ? v.X : v.Y;
Dim dim = dimension == Dimension.Width ? v.Width : v.Height;
// Check for not dependent views first (most common case)
if ((pos.IsFixed || dim.IsFixed) && !pos.DependsOnSuperViewContentSize && !dim.DependsOnSuperViewContentSize)
{
categories.NotDependent.Add (v);
}
// Check for centered views
if (pos.Has<PosCenter> (out _))
{
categories.Centered.Add (v);
}
// Check for anchored views
if (pos.Has<PosAnchorEnd> (out _))
{
categories.Anchored.Add (v);
}
// Check for PosView based views
if (pos.Has<PosView> (out _))
{
categories.PosViewBased.Add (v);
}
// Check for DimView based views
if (dim.Has<DimView> (out _))
{
categories.DimViewBased.Add (v);
}
// Check for DimAuto based views
if (dim.Has<DimAuto> (out _))
{
categories.DimAutoBased.Add (v);
}
// Check for DimFill based views that can contribute
if (dim.Has<DimFill> (out _) && dim.CanContributeToAutoSizing)
{
categories.DimFillBased.Add (v);
}
// Collect align group IDs
if (!pos.Has (out PosAlign posAlign))
{
continue;
}
if (seenAlignGroupIds.Add (posAlign.GroupId))
{
categories.AlignGroupIds.Add (posAlign.GroupId);
}
}
return categories;
}
/// <summary>
/// Calculates maximum size from a pre-categorized list of views.
/// Phase 1 and 2 Performance Optimization: Avoids redundant filtering.
/// </summary>
private static int CalculateMaxSizeFromList (List<View> views, int max, Dimension dimension)
{
foreach (View v in views)
{
int newMax = dimension == Dimension.Width
? v.Frame.X + v.Width.Calculate (0, max, v, dimension)
: v.Frame.Y + v.Height.Calculate (0, max, v, dimension);
if (newMax > max)
{
max = newMax;
}
}
return max;
}
/// <inheritdoc/>
internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension)
{
var textSize = 0;
@@ -46,8 +169,6 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
int screenX4 = dimension == Dimension.Width ? screenSize.Width * 4 : screenSize.Height * 4;
int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4;
//Debug.WriteLineIf (autoMin > autoMax, "MinimumContentDim must be less than or equal to MaximumContentDim.");
if (Style.FastHasFlags (DimAutoStyle.Text))
{
if (dimension == Dimension.Width)
@@ -83,8 +204,6 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
}
}
List<View> viewsNeedingLayout = [];
if (Style.FastHasFlags (DimAutoStyle.Content))
{
maxCalculatedSize = textSize;
@@ -96,51 +215,22 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
}
else
{
List<View> includedSubViews = us.InternalSubViews.ToList ();
List<View> notDependentSubViews;
// Single-pass categorization to reduce iterations and allocations
// Work directly with the collection to avoid unnecessary ToList() allocation
if (dimension == Dimension.Width)
{
notDependentSubViews = includedSubViews
.Where (v =>
(v.X is PosAbsolute or PosFunc
|| v.Width is DimAuto or DimAbsolute or DimFunc) // BUGBUG: We should use v.X.Has and v.Width.Has?
&& !v.X.DependsOnSuperViewContentSize
&& !v.Width.DependsOnSuperViewContentSize)
.ToList ();
}
else
{
notDependentSubViews = includedSubViews
.Where (v =>
(v.Y is PosAbsolute or PosFunc
|| v.Height is DimAuto or DimAbsolute or DimFunc) // BUGBUG: We should use v.Y.Has and v.Height.Has?
&& !v.Y.DependsOnSuperViewContentSize
&& !v.Height.DependsOnSuperViewContentSize)
.ToList ();
}
// Categorize views in a single pass
ViewCategories categories = CategorizeViews (us.InternalSubViews, dimension, superviewContentSize);
foreach (View notDependentSubView in notDependentSubViews)
// Process not-dependent views
foreach (View notDependentSubView in categories.NotDependent)
{
notDependentSubView.SetRelativeLayout (us.GetContentSize ());
}
for (var i = 0; i < notDependentSubViews.Count; i++)
{
View v = notDependentSubViews [i];
var size = 0;
if (dimension == Dimension.Width)
{
int width = v.Width.Calculate (0, superviewContentSize, v, dimension);
size = v.X.GetAnchor (0) + width;
}
else
{
int height = v.Height.Calculate (0, superviewContentSize, v, dimension);
size = v.Y.GetAnchor (0) + height;
}
int size = dimension == Dimension.Width
? notDependentSubView.X.GetAnchor (0)
+ notDependentSubView.Width.Calculate (0, superviewContentSize, notDependentSubView, dimension)
: notDependentSubView.Y.GetAnchor (0)
+ notDependentSubView.Height.Calculate (0, superviewContentSize, notDependentSubView, dimension);
if (size > maxCalculatedSize)
{
@@ -148,122 +238,38 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
}
}
// ************** We now have some idea of `us.ContentSize` ***************
#region Centered
// [ ] PosCenter - Position is dependent `us.ContentSize` AND `subview.Frame`
List<View> centeredSubViews;
if (dimension == Dimension.Width)
{
centeredSubViews = us.InternalSubViews.Where (v => v.X.Has<PosCenter> (out _)).ToList ();
}
else
{
centeredSubViews = us.InternalSubViews.Where (v => v.Y.Has<PosCenter> (out _)).ToList ();
}
viewsNeedingLayout.AddRange (centeredSubViews);
// Process centered views
var maxCentered = 0;
for (var i = 0; i < centeredSubViews.Count; i++)
foreach (View v in categories.Centered)
{
View v = centeredSubViews [i];
if (dimension == Dimension.Width)
{
int width = v.Width.Calculate (0, screenX4, v, dimension);
maxCentered = v.X.GetAnchor (0) + width;
}
else
{
int height = v.Height.Calculate (0, screenX4, v, dimension);
maxCentered = v.Y.GetAnchor (0) + height;
}
maxCentered = dimension == Dimension.Width
? v.X.GetAnchor (0) + v.Width.Calculate (0, screenX4, v, dimension)
: v.Y.GetAnchor (0) + v.Height.Calculate (0, screenX4, v, dimension);
}
maxCalculatedSize = int.Max (maxCalculatedSize, maxCentered);
#endregion Centered
#region Percent
// [ ] DimPercent - Dimension is dependent on `us.ContentSize`
// No need to do anything.
#endregion Percent
#region Aligned
// [ ] PosAlign - Position is dependent on other views with `GroupId` AND `us.ContentSize`
// Process aligned views
var maxAlign = 0;
// Use Linq to get a list of distinct GroupIds from the subviews
List<int> groupIds = includedSubViews.Select (v =>
{
return dimension switch
{
Dimension.Width when v.X.Has (out PosAlign posAlign) => posAlign.GroupId,
Dimension.Height when v.Y.Has (out PosAlign posAlign) => posAlign.GroupId,
_ => -1
};
})
.Distinct ()
.ToList ();
foreach (int groupId in groupIds.Where (g => g != -1))
foreach (int groupId in categories.AlignGroupIds)
{
// PERF: If this proves a perf issue, consider caching a ref to this list in each item
List<PosAlign?> posAlignsInGroup = includedSubViews.Where (v => PosAlign.HasGroupId (v, dimension, groupId))
.Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
.ToList ();
if (posAlignsInGroup.Count == 0)
{
continue;
}
maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubViews, dimension);
// Convert to IReadOnlyCollection for PosAlign API
maxAlign = PosAlign.CalculateMinDimension (groupId, us.InternalSubViews.ToArray (), dimension);
}
maxCalculatedSize = int.Max (maxCalculatedSize, maxAlign);
#endregion Aligned
#region Anchored
// [x] PosAnchorEnd - Position is dependent on `us.ContentSize` AND `subview.Frame`
List<View> anchoredSubViews;
if (dimension == Dimension.Width)
{
anchoredSubViews = includedSubViews.Where (v => v.X.Has<PosAnchorEnd> (out _)).ToList ();
}
else
{
anchoredSubViews = includedSubViews.Where (v => v.Y.Has<PosAnchorEnd> (out _)).ToList ();
}
viewsNeedingLayout.AddRange (anchoredSubViews);
// Process anchored views
var maxAnchorEnd = 0;
for (var i = 0; i < anchoredSubViews.Count; i++)
foreach (View anchoredSubView in categories.Anchored)
{
View anchoredSubView = anchoredSubViews [i];
// Need to set the relative layout for PosAnchorEnd subviews to calculate the size
// TODO: Figure out a way to not have to calculate change the state of subviews (calling SRL).
if (dimension == Dimension.Width)
{
anchoredSubView.SetRelativeLayout (new Size (maxCalculatedSize, screenX4));
}
else
{
anchoredSubView.SetRelativeLayout (new Size (screenX4, maxCalculatedSize));
}
anchoredSubView.SetRelativeLayout (dimension == Dimension.Width
? new Size (maxCalculatedSize, screenX4)
: new Size (screenX4, maxCalculatedSize));
maxAnchorEnd = dimension == Dimension.Width
? anchoredSubView.X.GetAnchor (maxCalculatedSize + anchoredSubView.Frame.Width)
@@ -272,125 +278,14 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
maxCalculatedSize = Math.Max (maxCalculatedSize, maxAnchorEnd);
#endregion Anchored
#region PosView
// [x] PosView - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
List<View> posViewSubViews;
if (dimension == Dimension.Width)
{
posViewSubViews = includedSubViews.Where (v => v.X.Has<PosView> (out _)).ToList ();
}
else
{
posViewSubViews = includedSubViews.Where (v => v.Y.Has<PosView> (out _)).ToList ();
}
for (var i = 0; i < posViewSubViews.Count; i++)
{
View v = posViewSubViews [i];
// BUGBUG: The order may not be correct. May need to call TopologicalSort?
// TODO: Figure out a way to not have to Calculate change the state of subviews (calling SRL).
int maxPosView = dimension == Dimension.Width
? v.Frame.X + v.Width.Calculate (0, maxCalculatedSize, v, dimension)
: v.Frame.Y + v.Height.Calculate (0, maxCalculatedSize, v, dimension);
if (maxPosView > maxCalculatedSize)
{
maxCalculatedSize = maxPosView;
}
}
#endregion PosView
// [x] PosCombine - Position is dependent if `Pos.Has ([one of the above]` - it can cause a change in `us.ContentSize`
#region DimView
// [x] DimView - Dimension is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
List<View> dimViewSubViews;
if (dimension == Dimension.Width)
{
dimViewSubViews = includedSubViews.Where (v => v.Width.Has<DimView> (out _)).ToList ();
}
else
{
dimViewSubViews = includedSubViews.Where (v => v.Height.Has<DimView> (out _)).ToList ();
}
for (var i = 0; i < dimViewSubViews.Count; i++)
{
View v = dimViewSubViews [i];
// BUGBUG: The order may not be correct. May need to call TopologicalSort?
// TODO: Figure out a way to not have to Calculate change the state of subviews (calling SRL).
int maxDimView = dimension == Dimension.Width
? v.Frame.X + v.Width.Calculate (0, maxCalculatedSize, v, dimension)
: v.Frame.Y + v.Height.Calculate (0, maxCalculatedSize, v, dimension);
if (maxDimView > maxCalculatedSize)
{
maxCalculatedSize = maxDimView;
}
}
#endregion DimView
#region DimAuto
// [ ] DimAuto - Dimension is internally calculated
List<View> dimAutoSubViews;
if (dimension == Dimension.Width)
{
dimAutoSubViews = includedSubViews.Where (v => v.Width.Has<DimAuto> (out _)).ToList ();
}
else
{
dimAutoSubViews = includedSubViews.Where (v => v.Height.Has<DimAuto> (out _)).ToList ();
}
for (var i = 0; i < dimAutoSubViews.Count; i++)
{
View v = dimAutoSubViews [i];
int maxDimAuto = dimension == Dimension.Width
? v.Frame.X + v.Width.Calculate (0, maxCalculatedSize, v, dimension)
: v.Frame.Y + v.Height.Calculate (0, maxCalculatedSize, v, dimension);
if (maxDimAuto > maxCalculatedSize)
{
maxCalculatedSize = maxDimAuto;
}
}
#endregion
#region DimFill
// DimFill subviews contribute to auto-sizing only if they have MinimumContentDim or To set
List<View> contributingDimFillSubViews;
if (dimension == Dimension.Width)
{
contributingDimFillSubViews = us.InternalSubViews.Where (v => v.Width.Has<DimFill> (out _) && v.Width.CanContributeToAutoSizing).ToList ();
}
else
{
contributingDimFillSubViews = us.InternalSubViews
.Where (v => v.Height.Has<DimFill> (out _) && v.Height.CanContributeToAutoSizing)
.ToList ();
}
// Process PosView, DimView, and DimAuto based views
maxCalculatedSize = CalculateMaxSizeFromList (categories.PosViewBased, maxCalculatedSize, dimension);
maxCalculatedSize = CalculateMaxSizeFromList (categories.DimViewBased, maxCalculatedSize, dimension);
maxCalculatedSize = CalculateMaxSizeFromList (categories.DimAutoBased, maxCalculatedSize, dimension);
// Process DimFill views that can contribute
for (var i = 0; i < contributingDimFillSubViews.Count; i++)
foreach (View dimFillSubView in categories.DimFillBased)
{
View dimFillSubView = contributingDimFillSubViews [i];
Dim dimFill = dimension == Dimension.Width ? dimFillSubView.Width : dimFillSubView.Height;
// Get the minimum contribution from the Dim itself
@@ -409,22 +304,22 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
}
// Handle special case for DimFill with To (still needs type-specific logic)
if (dimFill is DimFill dimFillTyped && dimFillTyped.To is { })
if (dimFill is not DimFill dimFillTyped || dimFillTyped.To is null)
{
// The SuperView needs to be large enough to contain both the dimFillSubView and the To view
int dimFillPos = dimension == Dimension.Width ? dimFillSubView.Frame.X : dimFillSubView.Frame.Y;
int toViewPos = dimension == Dimension.Width ? dimFillTyped.To.Frame.X : dimFillTyped.To.Frame.Y;
int toViewSize = dimension == Dimension.Width ? dimFillTyped.To.Frame.Width : dimFillTyped.To.Frame.Height;
int totalSize = int.Max (dimFillPos, toViewPos + toViewSize);
continue;
}
if (totalSize > maxCalculatedSize)
{
maxCalculatedSize = totalSize;
}
// The SuperView needs to be large enough to contain both the dimFillSubView and the To view
int dimFillPos = dimension == Dimension.Width ? dimFillSubView.Frame.X : dimFillSubView.Frame.Y;
int toViewPos = dimension == Dimension.Width ? dimFillTyped.To.Frame.X : dimFillTyped.To.Frame.Y;
int toViewSize = dimension == Dimension.Width ? dimFillTyped.To.Frame.Width : dimFillTyped.To.Frame.Height;
int totalSizeTo = int.Max (dimFillPos, toViewPos + toViewSize);
if (totalSizeTo > maxCalculatedSize)
{
maxCalculatedSize = totalSizeTo;
}
}
#endregion
}
}

View File

@@ -117,6 +117,12 @@ public record DimCombine (AddOrSubtract Add, Dim Left, Dim Right) : Dim
return newDimension;
}
/// <inheritdoc/>
internal override bool IsFixed => Left.IsFixed && Right.IsFixed;
/// <inheritdoc/>
internal override bool RequiresTargetLayout => Left.RequiresTargetLayout || Right.RequiresTargetLayout;
/// <inheritdoc/>
protected override bool HasInner<TDim> (out TDim dim) => Left.Has (out dim) || Right.Has (out dim);
}

View File

@@ -35,4 +35,7 @@ public record DimFunc (Func<View?, int> Fn, View? View = null) : Dim
yield return View;
}
}
/// <inheritdoc/>
internal override bool IsFixed => true;
}

View File

@@ -59,4 +59,7 @@ public record DimView : Dim
yield return Target;
}
}
/// <inheritdoc/>
internal override bool RequiresTargetLayout => true;
}

View File

@@ -416,6 +416,44 @@ public abstract record Pos
/// </returns>
internal virtual bool DependsOnSuperViewContentSize => false;
/// <summary>
/// Indicates whether this Pos has a fixed value that doesn't depend on layout calculations.
/// </summary>
/// <remarks>
/// <para>
/// This property is used by <see cref="DimAuto"/> to identify positions that can be
/// determined without performing layout calculations on other views.
/// </para>
/// <para>
/// Fixed positions include <see cref="PosAbsolute"/> and positions calculated by
/// <see cref="PosFunc"/> that don't depend on other views' layouts.
/// </para>
/// </remarks>
/// <returns>
/// <see langword="true"/> if this Pos has a fixed value independent of layout;
/// otherwise, <see langword="false"/>.
/// </returns>
internal virtual bool IsFixed => false;
/// <summary>
/// Indicates whether this Pos requires the target view to be laid out before it can be calculated.
/// </summary>
/// <remarks>
/// <para>
/// This property is used by <see cref="DimAuto"/> to identify positions that depend on
/// another view's layout being completed first.
/// </para>
/// <para>
/// Positions that require target layout include <see cref="PosView"/> which depends on
/// the target view's calculated position.
/// </para>
/// </remarks>
/// <returns>
/// <see langword="true"/> if this Pos requires the target view's layout to be calculated first;
/// otherwise, <see langword="false"/>.
/// </returns>
internal virtual bool RequiresTargetLayout => false;
/// <summary>
/// Indicates whether the specified type <typeparamref name="TPos"/> is in the hierarchy of this Pos object.
/// </summary>

View File

@@ -21,4 +21,7 @@ public record PosAbsolute (int Position) : Pos
public override string ToString () => $"Absolute({Position})";
internal override int GetAnchor (int size) => Position;
/// <inheritdoc/>
internal override bool IsFixed => true;
}

View File

@@ -86,6 +86,12 @@ public record PosCombine (AddOrSubtract Add, Pos Left, Pos Right) : Pos
/// <inheritdoc/>
internal override bool DependsOnSuperViewContentSize => Left.DependsOnSuperViewContentSize || Right.DependsOnSuperViewContentSize;
/// <inheritdoc/>
internal override bool IsFixed => Left.IsFixed && Right.IsFixed;
/// <inheritdoc/>
internal override bool RequiresTargetLayout => Left.RequiresTargetLayout || Right.RequiresTargetLayout;
/// <inheritdoc/>
protected override bool HasInner<TPos> (out TPos pos) => Left.Has (out pos) || Right.Has (out pos);
}

View File

@@ -34,4 +34,7 @@ public record PosFunc (Func<View?, int> Fn, View? View = null) : Pos
yield return View;
}
}
/// <inheritdoc/>
internal override bool IsFixed => true;
}

View File

@@ -69,4 +69,7 @@ public record PosView : Pos
{
yield return Target;
}
/// <inheritdoc/>
internal override bool RequiresTargetLayout => true;
}

View File

@@ -0,0 +1,188 @@
using BenchmarkDotNet.Attributes;
using Terminal.Gui.App;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
namespace Terminal.Gui.Benchmarks.Layout;
/// <summary>
/// Benchmarks for DimAuto performance testing.
/// Tests various scenarios to measure iteration overhead, allocation pressure, and overall execution time.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory ("DimAuto")]
public class DimAutoBenchmark
{
private View _simpleView = null!;
private View _complexView = null!;
private View _deeplyNestedView = null!;
[GlobalSetup]
public void Setup ()
{
// Initialize application context with ANSI driver for benchmarking
Application.Init (driverName: "ANSI");
// Simple scenario: Few subviews with basic positioning
_simpleView = CreateSimpleView ();
// Complex scenario: Many subviews with mixed Pos/Dim types
_complexView = CreateComplexView ();
// Deeply nested scenario: Nested views with DimAuto
_deeplyNestedView = CreateDeeplyNestedView ();
}
[GlobalCleanup]
public void Cleanup ()
{
Application.Shutdown ();
}
/// <summary>
/// Benchmark for simple layout with 3 subviews using basic positioning.
/// </summary>
[Benchmark (Baseline = true)]
public void SimpleLayout ()
{
_simpleView.SetNeedsLayout ();
_simpleView.Layout ();
}
/// <summary>
/// Benchmark for complex layout with 20 subviews using mixed Pos/Dim types.
/// Tests iteration overhead and categorization performance.
/// </summary>
[Benchmark]
public void ComplexLayout ()
{
_complexView.SetNeedsLayout ();
_complexView.Layout ();
}
/// <summary>
/// Benchmark for deeply nested layout with DimAuto at multiple levels.
/// Tests recursive layout performance.
/// </summary>
[Benchmark]
public void DeeplyNestedLayout ()
{
_deeplyNestedView.SetNeedsLayout ();
_deeplyNestedView.Layout ();
}
private View CreateSimpleView ()
{
var parent = new View
{
Width = Dim.Auto (),
Height = Dim.Auto ()
};
parent.Add (
new Label { X = 0, Y = 0, Text = "Label 1" },
new Label { X = 0, Y = 1, Text = "Label 2" },
new Button { X = 0, Y = 2, Text = "Button" }
);
return parent;
}
private View CreateComplexView ()
{
var parent = new View
{
Width = Dim.Auto (),
Height = Dim.Auto ()
};
// Mix of different positioning types
parent.Add (
// Absolute positioning
new Label { X = 0, Y = 0, Width = 20, Height = 1, Text = "Absolute" },
// DimAuto
new View
{
X = 0, Y = 1, Width = Dim.Auto (), Height = Dim.Auto ()
},
// PosCenter
new Label { X = Pos.Center (), Y = 2, Width = 15, Height = 1, Text = "Centered" },
// PosPercent
new Label { X = Pos.Percent (25), Y = 3, Width = 15, Height = 1, Text = "25%" },
// DimFill
new View { X = 0, Y = 4, Width = Dim.Fill (), Height = 3 },
// PosAnchorEnd
new Label { X = Pos.AnchorEnd (10), Y = 5, Width = 8, Height = 1, Text = "Anchored" },
// PosAlign
new Label { X = Pos.Align (Alignment.End), Y = 6, Width = 10, Height = 1, Text = "Aligned" },
// Multiple views with DimFunc
new Label { X = 0, Y = 7, Width = Dim.Func ((Func<View?, int>)(_ => 20)), Height = 1, Text = "Func 1" },
new Label { X = 0, Y = 8, Width = Dim.Func ((Func<View?, int>)(_ => 25)), Height = 1, Text = "Func 2" },
new Label { X = 0, Y = 9, Width = Dim.Func ((Func<View?, int>)(_ => 30)), Height = 1, Text = "Func 3" },
// Multiple views with DimPercent
new View { X = 0, Y = 10, Width = Dim.Percent (50), Height = 1 },
new View { X = 0, Y = 11, Width = Dim.Percent (75), Height = 1 },
// More absolute views
new Label { X = 0, Y = 14, Width = 18, Height = 1, Text = "Absolute 2" },
new Label { X = 0, Y = 15, Width = 22, Height = 1, Text = "Absolute 3" },
new Label { X = 0, Y = 16, Width = 16, Height = 1, Text = "Absolute 4" },
// DimFill with To
new View
{
X = 0, Y = 17,
Width = Dim.Fill (), Height = 1
}
);
// Add nested view after creation to avoid Subviews indexing issues
var nestedView = (View)parent.InternalSubViews [1];
nestedView.Add (new Label { X = 0, Y = 0, Text = "Nested Auto" });
return parent;
}
private View CreateDeeplyNestedView ()
{
var root = new View
{
Width = Dim.Auto (),
Height = Dim.Auto ()
};
View currentParent = root;
// Create 5 levels of nesting
for (var level = 0; level < 5; level++)
{
var container = new View
{
X = 0,
Y = level,
Width = Dim.Auto (),
Height = Dim.Auto ()
};
// Add some content at each level
container.Add (
new Label { X = 0, Y = 0, Text = $"Level {level} - Item 1" },
new Label { X = 0, Y = 1, Text = $"Level {level} - Item 2" },
new Button { X = 0, Y = 2, Text = $"Level {level} Button" }
);
currentParent.Add (container);
currentParent = container;
}
return root;
}
}

View File

@@ -0,0 +1,95 @@
# Terminal.Gui Benchmarks
This project contains performance benchmarks for Terminal.Gui using [BenchmarkDotNet](https://benchmarkdotnet.org/).
## Running Benchmarks
### Run All Benchmarks
```bash
cd Tests/Benchmarks
dotnet run -c Release
```
### Run Specific Benchmark Category
```bash
# Run only DimAuto benchmarks
dotnet run -c Release -- --filter '*DimAuto*'
# Run only TextFormatter benchmarks
dotnet run -c Release -- --filter '*TextFormatter*'
```
### Run Specific Benchmark Method
```bash
# Run only the ComplexLayout benchmark
dotnet run -c Release -- --filter '*DimAutoBenchmark.ComplexLayout*'
```
### Quick Run (Shorter but Less Accurate)
For faster iteration during development:
```bash
dotnet run -c Release -- --filter '*DimAuto*' -j short
```
### List Available Benchmarks
```bash
dotnet run -c Release -- --list flat
```
## DimAuto Benchmarks
The `DimAutoBenchmark` class tests layout performance with `Dim.Auto()` in various scenarios:
- **SimpleLayout**: Baseline with 3 subviews using basic positioning
- **ComplexLayout**: 20 subviews with mixed Pos/Dim types (tests iteration overhead)
- **DeeplyNestedLayout**: 5 levels of nested views with DimAuto (tests recursive performance)
### Example Output
```
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4602/23H2/2023Update/SunValley3)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.102
[Host] : .NET 10.0.1 (10.0.125.52708), X64 RyuJIT AVX2
DefaultJob : .NET 10.0.1 (10.0.125.52708), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|-------------------- |-----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
| SimpleLayout | 5.234 μs | 0.0421 μs | 0.0394 μs | 1.00 | 0.01 | 0.3586 | 3.03 KB | 1.00 |
| ComplexLayout | 42.561 μs | 0.8234 μs | 0.7701 μs | 8.13 | 0.17 | 2.8076 | 23.45 KB | 7.74 |
| DeeplyNestedLayout | 25.123 μs | 0.4892 μs | 0.4577 μs | 4.80 | 0.10 | 1.7090 | 14.28 KB | 4.71 |
```
## Adding New Benchmarks
1. Create a new class in an appropriate subdirectory (e.g., `Layout/`, `Text/`)
2. Add the `[MemoryDiagnoser]` attribute to measure allocations
3. Add `[BenchmarkCategory("CategoryName")]` to group related benchmarks
4. Mark baseline scenarios with `[Benchmark(Baseline = true)]`
5. Use `[GlobalSetup]` and `[GlobalCleanup]` for initialization/cleanup
## Best Practices
- Always run benchmarks in **Release** configuration
- Run multiple iterations for accurate results (default is better than `-j short`)
- Use `[ArgumentsSource]` for parametrized benchmarks
- Include baseline scenarios for comparison
- Document what each benchmark measures
## Continuous Integration
Benchmarks are not run automatically in CI. Run them locally when:
- Making performance-critical changes
- Implementing performance optimizations
- Before releasing a new version
## Resources
- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/)
- [Performance Analysis Plan](../../plans/dimauto-perf-plan.md)

View File

@@ -0,0 +1,186 @@
#nullable disable
// Claude - Opus 4.5
namespace ViewBaseTests.Layout;
/// <summary>
/// Tests for Phase 5 categorization properties: IsFixed and RequiresTargetLayout.
/// These properties help DimAuto categorize Pos/Dim types without type checking.
/// </summary>
public class CategorizationPropertiesTests
{
#region IsFixed Tests - Dim
[Fact]
public void DimAbsolute_IsFixed ()
{
Dim dim = Dim.Absolute (42);
Assert.True (dim.IsFixed);
}
[Fact]
public void DimFunc_IsFixed ()
{
Dim dim = Dim.Func (_ => 25);
Assert.True (dim.IsFixed);
}
[Fact]
public void DimAuto_IsFixed ()
{
Dim dim = Dim.Auto ();
Assert.True (dim.IsFixed);
}
[Fact]
public void DimPercent_IsNotFixed ()
{
Dim dim = Dim.Percent (50);
Assert.False (dim.IsFixed);
}
[Fact]
public void DimFill_IsNotFixed ()
{
Dim dim = Dim.Fill ();
Assert.False (dim.IsFixed);
}
[Fact]
public void DimView_IsNotFixed ()
{
View view = new ();
Dim dim = Dim.Width (view);
Assert.False (dim.IsFixed);
}
#endregion
#region IsFixed Tests - Pos
[Fact]
public void PosAbsolute_IsFixed ()
{
Pos pos = Pos.Absolute (10);
Assert.True (pos.IsFixed);
}
[Fact]
public void PosFunc_IsFixed ()
{
Pos pos = Pos.Func (_ => 15);
Assert.True (pos.IsFixed);
}
[Fact]
public void PosCenter_IsNotFixed ()
{
Pos pos = Pos.Center ();
Assert.False (pos.IsFixed);
}
[Fact]
public void PosPercent_IsNotFixed ()
{
Pos pos = Pos.Percent (50);
Assert.False (pos.IsFixed);
}
[Fact]
public void PosAnchorEnd_IsNotFixed ()
{
Pos pos = Pos.AnchorEnd ();
Assert.False (pos.IsFixed);
}
[Fact]
public void PosView_IsNotFixed ()
{
View view = new ();
Pos pos = Pos.Left (view);
Assert.False (pos.IsFixed);
}
#endregion
#region RequiresTargetLayout Tests - Dim
[Fact]
public void DimView_RequiresTargetLayout ()
{
View view = new ();
Dim dim = Dim.Width (view);
Assert.True (dim.RequiresTargetLayout);
}
[Fact]
public void DimAbsolute_DoesNotRequireTargetLayout ()
{
Dim dim = Dim.Absolute (42);
Assert.False (dim.RequiresTargetLayout);
}
[Fact]
public void DimFunc_DoesNotRequireTargetLayout ()
{
Dim dim = Dim.Func (_ => 25);
Assert.False (dim.RequiresTargetLayout);
}
[Fact]
public void DimPercent_DoesNotRequireTargetLayout ()
{
Dim dim = Dim.Percent (50);
Assert.False (dim.RequiresTargetLayout);
}
[Fact]
public void DimFill_DoesNotRequireTargetLayout ()
{
Dim dim = Dim.Fill ();
Assert.False (dim.RequiresTargetLayout);
}
#endregion
#region RequiresTargetLayout Tests - Pos
[Fact]
public void PosView_RequiresTargetLayout ()
{
View view = new ();
Pos pos = Pos.Left (view);
Assert.True (pos.RequiresTargetLayout);
}
[Fact]
public void PosAbsolute_DoesNotRequireTargetLayout ()
{
Pos pos = Pos.Absolute (10);
Assert.False (pos.RequiresTargetLayout);
}
[Fact]
public void PosFunc_DoesNotRequireTargetLayout ()
{
Pos pos = Pos.Func (_ => 15);
Assert.False (pos.RequiresTargetLayout);
}
[Fact]
public void PosCenter_DoesNotRequireTargetLayout ()
{
Pos pos = Pos.Center ();
Assert.False (pos.RequiresTargetLayout);
}
[Fact]
public void PosPercent_DoesNotRequireTargetLayout ()
{
Pos pos = Pos.Percent (50);
Assert.False (pos.RequiresTargetLayout);
}
#endregion
}

View File

@@ -347,3 +347,15 @@ If you encounter unexpected sizing with `Dim.Auto`, consider the following debug
- **Inspect Text Formatting**: For `Text` style, check `TextFormatter` settings and constraints (`ConstrainToWidth`, `ConstrainToHeight`). Ensure text is formatted correctly before sizing calculations.
By understanding the intricacies of `Dim.Auto` as implemented in Terminal.Gui v2, developers can create responsive and adaptive terminal UIs that automatically adjust to content changes, enhancing user experience and maintainability.
## Internal Architecture
`Dim.Auto` uses a polymorphic design to minimize coupling with specific `Pos` and `Dim` types. The layout system uses virtual properties and methods to categorize and process layout elements:
- **`DependsOnSuperViewContentSize`**: Identifies types that actively contribute to content size determination (e.g., `DimPercent`, `DimFill`, `PosAnchorEnd`, `PosAlign`)
- **`CanContributeToAutoSizing`**: Indicates whether a `Dim` can meaningfully contribute to auto-sizing (returns `false` for `DimPercent` and `DimFill` without `MinimumContentDim`/`To`)
- **`GetMinimumContribution()`**: Calculates the minimum size contribution during auto-sizing (overridden by `DimFill` to return its `MinimumContentDim`)
- **`IsFixed`**: Identifies fixed-value types that don't depend on layout calculations (`DimAbsolute`, `PosAbsolute`, `DimFunc`, `PosFunc`, `DimAuto`)
- **`RequiresTargetLayout`**: Indicates types requiring target view layout first (`DimView`, `PosView`)
This design allows new `Pos`/`Dim` types to be added without modifying `DimAuto.Calculate()`.

View File

@@ -95,6 +95,8 @@ The flags are organized into categories:
Terminal.Gui provides a rich system for how views are laid out relative to each other. The position of a view is set by setting the `X` and `Y` properties, which are of time @Terminal.Gui.Pos. The size is set via `Width` and `Height`, which are of type @Terminal.Gui.Dim.
The layout system uses virtual properties for categorization without type checking: `ReferencesOtherViews()`, `DependsOnSuperViewContentSize`, `CanContributeToAutoSizing`, `GetMinimumContribution()`, `IsFixed`, and `RequiresTargetLayout`. This enables extensibility.
```cs
var label1 = new Label () { X = 1, Y = 2, Width = 3, Height = 4, Title = "Absolute")