Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
777c5e2
Add design plan for LinearRange IValue<T> refactor and clet
claude May 6, 2026
acb2a93
Refactor LinearRange to implement IValue<T> with three subclasses
Copilot May 6, 2026
2a4fb72
Remove redundant with {} expression in LinearRange.SpanFromIndices
Copilot May 6, 2026
03fa634
Address PR review: split tests, add visual tests, fix mouse drag, rem…
Copilot May 6, 2026
b641a89
Fix CI: update HardCoded cache test to new LinearRangeDefaults config…
Copilot May 6, 2026
c2004de
Merge branch 'develop' into claude/design-linearrange-clet-Zn29d
tig May 6, 2026
534cb68
Add IDesignable.EnableForDesign() to each LinearRange subclass with m…
Copilot May 6, 2026
18c1c55
Fix mouse drag for LinearRange: anchor-based drag for Closed; set sem…
Copilot May 6, 2026
bf448fd
Add non-generic LinearSelector / LinearMultiSelector / LinearRange so…
Copilot May 6, 2026
fea631a
Default DragChar to ContinuousMeterSegment so it matches SetChar
Copilot May 6, 2026
970f5b3
Remove redundant ClearViewport in LinearRange draw to fix drag flicker
Copilot May 6, 2026
6285cce
Potential fix for pull request finding
tig May 6, 2026
ce7ae27
Potential fix for pull request finding
tig May 6, 2026
316079c
Add SelectedIndex to LinearSelector for unambiguous "no selection" wi…
Copilot May 6, 2026
4b63655
Use HashSet to dedupe indices in LinearMultiSelector.Value setter (O(…
Copilot May 6, 2026
17f0a48
Merge branch 'develop' into claude/design-linearrange-clet-Zn29d
tig May 6, 2026
9e60694
Merge branch 'develop' into claude/design-linearrange-clet-Zn29d
tig May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Examples/UICatalog/Scenarios/DimAutoDemo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,10 @@ private static FrameView CreateSliderFrameView ()

List<object> options = ["One", "Two", "Three", "Four"];

LinearRange linearRange = new (options)
LinearMultiSelector<object> linearRange = new (options)
{
X = 0,
Y = 0,
Type = LinearRangeType.Multiple,
AllowEmpty = false,
BorderStyle = LineStyle.Double,
Title = "_LinearRange"
Expand Down
634 changes: 77 additions & 557 deletions Examples/UICatalog/Scenarios/LinearRanges.cs

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Examples/UICatalog/Scenarios/Shortcuts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,24 +205,24 @@ private void HandleOnIsRunningChanged (object? sender, EventArgs<bool> e)
Y = Pos.Bottom (diagnosticShortcut),
Width = Dim.Fill (eventLog),
HelpText = "LinearRanges work!",
CommandView = new LinearRange<string> { Id = "sliderLR", Orientation = Orientation.Horizontal, AllowEmpty = true },
CommandView = new LinearMultiSelector<string> { Id = "sliderLR", Orientation = Orientation.Horizontal, AllowEmpty = true },
Key = Key.F5
};

((LinearRange<string>)sliderShortcut.CommandView).Options =
((LinearMultiSelector<string>)sliderShortcut.CommandView).Options =
[
new LinearRangeOption<string> { Legend = "A" }, new LinearRangeOption<string> { Legend = "B" }, new LinearRangeOption<string> { Legend = "C" }
];
((LinearRange<string>)sliderShortcut.CommandView).SetOption (0);
((LinearMultiSelector<string>)sliderShortcut.CommandView).Value = ["A"];

((LinearRange<string>)sliderShortcut.CommandView).OptionsChanged += (send, _) =>
((LinearMultiSelector<string>)sliderShortcut.CommandView).ValueChanged += (send, args) =>
{
if (send is LinearRange<string> lr)
if (send is LinearMultiSelector<string> lr)
{
eventLog.Log ($"OptionsChanged: {
eventLog.Log ($"ValueChanged: {
lr.GetType ().Name
} - {
string.Join (",", lr.GetSetOptions ())
string.Join (",", args.NewValue ?? [])
}");
}
};
Expand Down
3 changes: 1 addition & 2 deletions Examples/UICatalog/Scenarios/ViewportSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,11 @@ public override void Main ()

List<object> options = ["Option 1", "Option 2", "Option 3"];

LinearRange linearRange = new (options)
LinearMultiSelector<object> linearRange = new (options)
{
X = 0,
Y = Pos.Bottom (textField) + 1,
Orientation = Orientation.Vertical,
Type = LinearRangeType.Multiple,
AllowEmpty = false,
BorderStyle = LineStyle.Double,
Title = "_LinearRange"
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ internal static class ConfigPropertyHostTypes
typeof (FileDialogStyle),
typeof (FrameView),
typeof (HexView),
typeof (LinearRange),
typeof (LinearRangeDefaults),
typeof (Menu),
typeof (MenuBar),
typeof (MessageBox),
Expand Down Expand Up @@ -89,7 +89,7 @@ internal static class ConfigPropertyHostTypes
[DynamicDependency (PreservedMembers, typeof (FileDialogStyle))]
[DynamicDependency (PreservedMembers, typeof (FrameView))]
[DynamicDependency (PreservedMembers, typeof (HexView))]
[DynamicDependency (PreservedMembers, typeof (LinearRange))]
[DynamicDependency (PreservedMembers, typeof (LinearRangeDefaults))]
[DynamicDependency (PreservedMembers, typeof (Menu))]
[DynamicDependency (PreservedMembers, typeof (MenuBar))]
[DynamicDependency (PreservedMembers, typeof (MessageBox))]
Expand Down
24 changes: 24 additions & 0 deletions Terminal.Gui/Views/LinearRange/LinearMultiSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Terminal.Gui.Views;

/// <summary>
/// Convenience non-generic <see cref="LinearMultiSelector{T}"/> closed over <see cref="string"/>.
/// Allows designer scenarios (e.g. <c>AllViewsTester</c>) and reflection-based instantiation to
/// discover and create the view without supplying a type argument.
/// </summary>
/// <remarks>
/// <para>
/// To work with non-string option types, use <see cref="LinearMultiSelector{T}"/> directly.
/// </para>
/// </remarks>
public class LinearMultiSelector : LinearMultiSelector<string>
{
/// <summary>Initializes a new instance of <see cref="LinearMultiSelector"/>.</summary>
public LinearMultiSelector () { }

/// <summary>Initializes a new instance of <see cref="LinearMultiSelector"/>.</summary>
/// <param name="options">Initial options.</param>
/// <param name="orientation">Initial orientation.</param>
public LinearMultiSelector (List<string>? options, Orientation orientation = Orientation.Horizontal)
: base (options, orientation)
{ }
}
165 changes: 165 additions & 0 deletions Terminal.Gui/Views/LinearRange/LinearMultiSelectorT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
namespace Terminal.Gui.Views;

/// <summary>
/// A linear range view that allows selection of zero or more options from a typed list.
/// </summary>
/// <typeparam name="T">The data type of the options.</typeparam>
/// <remarks>
/// <para>
/// Exposes the current selection through <see cref="Value"/> as
/// <see cref="IReadOnlyList{T}"/>; the list is empty when no options are selected.
/// A defensive copy is taken when <see cref="Value"/> is set, so the caller may mutate
/// the list passed in without affecting subsequent reads.
/// </para>
/// <para>
/// Equality between the current value and a new value uses
/// <see cref="System.Linq.Enumerable.SequenceEqual{T}(IEnumerable{T},IEnumerable{T})"/>,
/// so two distinct list instances with the same elements in the same order are considered equal.
/// </para>
/// </remarks>
public class LinearMultiSelector<T> : LinearRangeViewBase<T, IReadOnlyList<T>>, IDesignable
{
private static readonly IReadOnlyList<T> _emptyList = new List<T> (0).AsReadOnly ();

private IReadOnlyList<T> _value = _emptyList;

/// <summary>Initializes a new instance of <see cref="LinearMultiSelector{T}"/>.</summary>
public LinearMultiSelector () : base (LinearRangeRenderMode.Multiple) { }

/// <summary>Initializes a new instance of <see cref="LinearMultiSelector{T}"/>.</summary>
/// <param name="options">Initial options.</param>
/// <param name="orientation">Initial orientation.</param>
public LinearMultiSelector (List<T>? options, Orientation orientation = Orientation.Horizontal)
: base (options, orientation, LinearRangeRenderMode.Multiple) { }

/// <inheritdoc/>
/// <remarks>
/// The setter accepts <see langword="null"/> as a synonym for an empty list. The getter never
/// returns <see langword="null"/>.
/// </remarks>
public override IReadOnlyList<T>? Value
{
get => _value;
set
{
IReadOnlyList<T> incoming = value is null ? _emptyList : new List<T> (value).AsReadOnly ();
IReadOnlyList<T> current = _value;

if (SequenceEqualByDefault (current, incoming))
{
return;
}

if (RaiseValueChanging (current, incoming))
{
return;
}

_value = incoming;

// Sync indices: find the option index for each element of incoming.
// Use a HashSet to dedupe in O(1) per item rather than O(n) List.Contains scans.
List<int> indices = new (incoming.Count);
HashSet<int> seen = new (incoming.Count);

foreach (T item in incoming)
{
int idx = IndexOfData (item);

if (idx >= 0 && seen.Add (idx))
{
indices.Add (idx);
}
}
Comment thread
tig marked this conversation as resolved.

ApplySelectedIndices (indices);

RaiseValueChanged (current, _value);
}
}

/// <inheritdoc/>
protected override void OnSelectionChanged ()
{
IReadOnlyList<T> previous = _value;

// Build the new value from current indices in the order they appear in Options
// (rather than the order they were selected) for stable, predictable output.
IReadOnlyList<int> indices = SelectedIndices;
List<T> next = new (indices.Count);
List<int> ordered = new (indices);
ordered.Sort ();

foreach (int i in ordered)
{
if (i >= 0 && i < Options.Count)
{
next.Add (Options [i].Data!);
}
}

IReadOnlyList<T> newValue = next.AsReadOnly ();

if (SequenceEqualByDefault (previous, newValue))
{
return;
}

_value = newValue;
RaiseValueChanged (previous, newValue);
}

private static bool SequenceEqualByDefault (IReadOnlyList<T> a, IReadOnlyList<T> b)
{
if (ReferenceEquals (a, b))
{
return true;
}

if (a.Count != b.Count)
{
return false;
}

EqualityComparer<T> cmp = EqualityComparer<T>.Default;

for (var i = 0; i < a.Count; i++)
{
if (!cmp.Equals (a [i], b [i]))
{
return false;
}
}

return true;
}

/// <summary>
/// Loads demo data suitable for a designer preview: a multi-select
/// <see cref="LinearMultiSelector{T}"/> of the seven days of the week, with the five weekdays
/// (Mon–Fri) preselected. Only populated when <typeparamref name="T"/> is <see cref="string"/>;
/// for any other type, the view is left untouched and <see langword="false"/> is returned.
/// </summary>
/// <returns><see langword="true"/> if demo data was loaded.</returns>
public virtual bool EnableForDesign ()
{
if (typeof (T) != typeof (string))
{
return false;
}

Title = "Active Days";
AssignHotKeys = true;
ShowLegends = true;

string [] days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

Options = days.Select (
d => new LinearRangeOption<T> (d, (Rune)d [0], (T)(object)d))
.ToList ();

Value = days.Take (5).Select (d => (T)(object)d).ToList ();

return true;
}
}
Loading
Loading