mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-02-10 04:03:41 +01:00
Merge branch 'v2_develop' into copilot/fix-mouse-event-routing-issue
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,4 +35,7 @@ public record DimFunc (Func<View?, int> Fn, View? View = null) : Dim
|
||||
yield return View;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override bool IsFixed => true;
|
||||
}
|
||||
|
||||
@@ -59,4 +59,7 @@ public record DimView : Dim
|
||||
yield return Target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override bool RequiresTargetLayout => true;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -34,4 +34,7 @@ public record PosFunc (Func<View?, int> Fn, View? View = null) : Pos
|
||||
yield return View;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override bool IsFixed => true;
|
||||
}
|
||||
|
||||
@@ -69,4 +69,7 @@ public record PosView : Pos
|
||||
{
|
||||
yield return Target;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal override bool RequiresTargetLayout => true;
|
||||
}
|
||||
|
||||
188
Tests/Benchmarks/Layout/DimAutoBenchmark.cs
Normal file
188
Tests/Benchmarks/Layout/DimAutoBenchmark.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
95
Tests/Benchmarks/README.md
Normal file
95
Tests/Benchmarks/README.md
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()`.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user