From fb4043afbce6c4f57d7c7e5b284df3f8749b110b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:40:09 -0700 Subject: [PATCH] Fixes #3829. Rename Slider to LinearRange and implement CWP (#4509) * Initial plan * Rename Slider to LinearRange - files and code updated Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix variable name mismatches in scenario files Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix typos in comments identified by code review Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update HelpText for consistency with LinearRange rename Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor LinearRange properties to use CWP with CWPPropertyHelper - Added CWP events (Changing/Changed) for Type, LegendsOrientation, MinimumInnerSpacing, ShowLegends, ShowEndSpacing, and UseMinimumSize properties - Used CWPPropertyHelper for consistent property change workflow - Added virtual OnChanging/OnChanged methods for each property - All existing tests pass Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix CWP property implementations - remove duplicate field updates in doWork - CWPPropertyHelper updates the ref parameter, so doWork should only contain side effects - Fixed Type, MinimumInnerSpacing, LegendsOrientation, ShowLegends, ShowEndSpacing, and UseMinimumSize - Added comprehensive CWP tests for property changes (9 new tests, all passing) - All 45 pre-existing LinearRange tests still pass (10 were already failing before changes) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Add fluent integration tests for LinearRange - Created LinearRangeFluentTests with 5 test scenarios - Tests cover rendering, navigation, type changes with CWP events, range selection, and vertical orientation - All tests build successfully Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix unit tests and address review comments - Fixed review comments: Updated Title properties from "_Slider" to "_LinearRange" in DimAutoDemo and ViewportSettings - Fixed review comment: Renamed filterSlider to filterLinearRange in Mouse.cs - Fixed CWP implementation: Use local variable instead of backing field as ref parameter to CWPPropertyHelper - This ensures doWork can update the backing field BEFORE SetContentSize() reads it - All 55 LinearRange tests now passing (was 45/55, now 55/55) Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update DimAutoDemo.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update ViewportSettings.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * code cleanup * Refactor LinearRanges scenario for clarity and modularity Refactored the LinearRanges scenario to separate UI setup, configuration, and event logic. The Main() method now handles all UI layout, configuration controls, and event wiring, while MakeSliders() is responsible only for adding LinearRange controls to the main window. Updated event handlers to use discard parameters for unused arguments and modernized code with collection initializers and LINQ. This results in a clearer separation of concerns and improved maintainability. * un did change * cleanup * cleanup * cleanup * fixed unit test code cleanup --------- Co-authored-by: Tig Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/Scenarios/DimAutoDemo.cs | 10 +- Examples/UICatalog/Scenarios/LinearRanges.cs | 615 +++++++++++++ Examples/UICatalog/Scenarios/Shortcuts.cs | 12 +- Examples/UICatalog/Scenarios/Sliders.cs | 621 ------------- .../UICatalog/Scenarios/ViewportSettings.cs | 8 +- .../Slider.cs => LinearRange/LinearRange.cs} | 831 ++++++++++-------- .../LinearRangeAttributes.cs} | 4 +- .../LinearRangeConfiguration.cs} | 9 +- .../LinearRangeEventArgs.cs} | 11 +- .../Views/LinearRange/LinearRangeOption.cs | 50 ++ .../LinearRange/LinearRangeOptionEventArgs.cs | 12 + .../LinearRangeStyle.cs} | 25 +- .../LinearRangeType.cs} | 4 +- Terminal.Gui/Views/Slider/SliderOption.cs | 51 -- .../Views/Slider/SliderOptionEventArgs.cs | 13 - .../FluentTests/LinearRangeFluentTests.cs | 144 +++ .../ViewBase/Adornment/PaddingTests.cs | 9 +- .../{SliderTests.cs => LinearRangeTests.cs} | 376 ++++++-- 18 files changed, 1620 insertions(+), 1185 deletions(-) create mode 100644 Examples/UICatalog/Scenarios/LinearRanges.cs delete mode 100644 Examples/UICatalog/Scenarios/Sliders.cs rename Terminal.Gui/Views/{Slider/Slider.cs => LinearRange/LinearRange.cs} (63%) rename Terminal.Gui/Views/{Slider/SliderAttributes.cs => LinearRange/LinearRangeAttributes.cs} (80%) rename Terminal.Gui/Views/{Slider/SliderConfiguration.cs => LinearRange/LinearRangeConfiguration.cs} (63%) rename Terminal.Gui/Views/{Slider/SliderEventArgs.cs => LinearRange/LinearRangeEventArgs.cs} (60%) create mode 100644 Terminal.Gui/Views/LinearRange/LinearRangeOption.cs create mode 100644 Terminal.Gui/Views/LinearRange/LinearRangeOptionEventArgs.cs rename Terminal.Gui/Views/{Slider/SliderStyle.cs => LinearRange/LinearRangeStyle.cs} (64%) rename Terminal.Gui/Views/{Slider/SliderType.cs => LinearRange/LinearRangeType.cs} (91%) delete mode 100644 Terminal.Gui/Views/Slider/SliderOption.cs delete mode 100644 Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs create mode 100644 Tests/IntegrationTests/FluentTests/LinearRangeFluentTests.cs rename Tests/UnitTestsParallelizable/Views/{SliderTests.cs => LinearRangeTests.cs} (50%) diff --git a/Examples/UICatalog/Scenarios/DimAutoDemo.cs b/Examples/UICatalog/Scenarios/DimAutoDemo.cs index ab97889d0..8fdef585e 100644 --- a/Examples/UICatalog/Scenarios/DimAutoDemo.cs +++ b/Examples/UICatalog/Scenarios/DimAutoDemo.cs @@ -179,20 +179,20 @@ public class DimAutoDemo : Scenario { var sliderFrameView = new FrameView { - Title = "Slider - Example of a DimAuto View", + Title = "LinearRange - Example of a DimAuto View", }; List options = new () { "One", "Two", "Three", "Four" }; - Slider slider = new (options) + LinearRange linearRange = new (options) { X = 0, Y = 0, - Type = SliderType.Multiple, + Type = LinearRangeType.Multiple, AllowEmpty = false, BorderStyle = LineStyle.Double, - Title = "_Slider" + Title = "_LinearRange" }; - sliderFrameView.Add (slider); + sliderFrameView.Add (linearRange); return sliderFrameView; } diff --git a/Examples/UICatalog/Scenarios/LinearRanges.cs b/Examples/UICatalog/Scenarios/LinearRanges.cs new file mode 100644 index 000000000..eb3c9ddb5 --- /dev/null +++ b/Examples/UICatalog/Scenarios/LinearRanges.cs @@ -0,0 +1,615 @@ +using System.Collections.ObjectModel; +using System.Text; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("LinearRanges", "Demonstrates the LinearRange view.")] +[ScenarioCategory ("Controls")] +public class LinearRanges : Scenario +{ + public override void Main () + { + Application.Init (); + using IApplication app = Application.Instance; + + using Window mainWindow = new (); + mainWindow.Title = GetQuitKeyAndName (); + + MakeSliders ( + mainWindow, + [ + 500, + 1000, + 1500, + 2000, + 2500, + 3000, + 3500, + 4000, + 4500, + 5000 + ] + ); + + FrameView configView = new () + { + Title = "Confi_guration", + X = Pos.Percent (50), + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (), + SchemeName = "Dialog" + }; + + mainWindow.Add (configView); + + #region Config LinearRange + + LinearRange optionsSlider = new () + { + Title = "Options", + X = 0, + Y = 0, + Width = Dim.Fill (), + Type = LinearRangeType.Multiple, + AllowEmpty = true, + BorderStyle = LineStyle.Single + }; + + optionsSlider.Style.SetChar = optionsSlider.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; + optionsSlider.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Black); + + optionsSlider.Options = + [ + new () { Legend = "Legends" }, + new () { Legend = "RangeAllowSingle" }, + new () { Legend = "EndSpacing" }, + new () { Legend = "DimAuto" } + ]; + + configView.Add (optionsSlider); + + optionsSlider.OptionsChanged += OnOptionsSliderOnOptionsChanged; + optionsSlider.SetOption (0); // Legends + optionsSlider.SetOption (1); // RangeAllowSingle + optionsSlider.SetOption (3); // DimAuto + + CheckBox dimAutoUsesMin = new () + { + Text = "Use minimum size (vs. ideal)", + X = 0, + Y = Pos.Bottom (optionsSlider) + }; + + dimAutoUsesMin.CheckedStateChanging += (_, _) => + { + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.UseMinimumSize = !s.UseMinimumSize; + } + }; + configView.Add (dimAutoUsesMin); + + #region LinearRange Orientation LinearRange + + LinearRange orientationSlider = new (new () { "Horizontal", "Vertical" }) + { + Title = "LinearRange Orientation", + X = 0, + Y = Pos.Bottom (dimAutoUsesMin) + 1, + BorderStyle = LineStyle.Single + }; + + orientationSlider.SetOption (0); + + configView.Add (orientationSlider); + + orientationSlider.OptionsChanged += OnOrientationSliderOnOptionsChanged; + + #endregion LinearRange Orientation LinearRange + + #region Legends Orientation LinearRange + + LinearRange legendsOrientationSlider = new (["Horizontal", "Vertical"]) + { + Title = "Legends Orientation", + X = 0, + Y = Pos.Bottom (orientationSlider) + 1, + BorderStyle = LineStyle.Single + }; + + legendsOrientationSlider.SetOption (0); + + configView.Add (legendsOrientationSlider); + + legendsOrientationSlider.OptionsChanged += OnLegendsOrientationSliderOnOptionsChanged; + + #endregion Legends Orientation LinearRange + + #region Spacing Options + + FrameView spacingOptions = new () + { + Title = "Spacing Options", + X = Pos.Right (orientationSlider), + Y = Pos.Top (orientationSlider), + Width = Dim.Fill (), + Height = Dim.Auto (), + BorderStyle = LineStyle.Single + }; + + Label label = new () + { + Text = "Min _Inner Spacing:" + }; + + NumericUpDown innerSpacingUpDown = new () + { + X = Pos.Right (label) + 1 + }; + + innerSpacingUpDown.Value = mainWindow.SubViews.OfType ().First ().MinimumInnerSpacing; + + innerSpacingUpDown.ValueChanging += (_, e) => + { + if (e.NewValue < 0) + { + e.Cancel = true; + + return; + } + + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.MinimumInnerSpacing = e.NewValue; + } + }; + + spacingOptions.Add (label, innerSpacingUpDown); + configView.Add (spacingOptions); + + #endregion + + #region Color LinearRange + + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.Style.OptionChar = s.Style.OptionChar with { Attribute = mainWindow.GetAttributeForRole (VisualRole.Normal) }; + s.Style.SetChar = s.Style.SetChar with { Attribute = mainWindow.GetAttributeForRole (VisualRole.Normal) }; + s.Style.LegendAttributes.SetAttribute = mainWindow.GetAttributeForRole (VisualRole.Normal); + s.Style.RangeChar = s.Style.RangeChar with { Attribute = mainWindow.GetAttributeForRole (VisualRole.Normal) }; + } + + LinearRange<(Color, Color)> sliderFgColor = new () + { + Title = "FG Color", + X = 0, + Y = Pos.Bottom (legendsOrientationSlider) + + 1, + Type = LinearRangeType.Single, + BorderStyle = LineStyle.Single, + AllowEmpty = false, + Orientation = Orientation.Vertical, + LegendsOrientation = Orientation.Horizontal, + MinimumInnerSpacing = 0, + UseMinimumSize = true + }; + + sliderFgColor.Style.SetChar = sliderFgColor.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; + sliderFgColor.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Blue); + + List> colorOptions = []; + + colorOptions.AddRange ( + from colorIndex in Enum.GetValues () + let colorName = colorIndex.ToString () + select new LinearRangeOption<(Color, Color)> + { Data = (new (colorIndex), new (colorIndex)), Legend = colorName, LegendAbbr = (Rune)colorName [0] }); + + sliderFgColor.Options = colorOptions; + + configView.Add (sliderFgColor); + + sliderFgColor.OptionsChanged += OnSliderFgColorOnOptionsChanged; + + LinearRange<(Color, Color)> sliderBgColor = new () + { + Title = "BG Color", + X = Pos.Right (sliderFgColor), + Y = Pos.Top (sliderFgColor), + Type = LinearRangeType.Single, + BorderStyle = LineStyle.Single, + AllowEmpty = false, + Orientation = Orientation.Vertical, + LegendsOrientation = Orientation.Horizontal, + MinimumInnerSpacing = 0, + UseMinimumSize = true + }; + + sliderBgColor.Style.SetChar = sliderBgColor.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; + sliderBgColor.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Blue); + + sliderBgColor.Options = colorOptions; + + configView.Add (sliderBgColor); + + sliderBgColor.OptionsChanged += (_, e) => + { + if (e.Options.Count == 0) + { + return; + } + + (Color, Color) data = e.Options.First ().Value.Data; + + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.SetScheme ( + new (s.GetScheme ()) + { + Normal = new ( + s.GetAttributeForRole (VisualRole.Normal).Foreground, + data.Item2 + ) + }); + } + }; + + #endregion Color LinearRange + + #endregion Config LinearRange + + ObservableCollection eventSource = []; + + ListView eventLog = new () + { + X = Pos.Right (sliderBgColor), + Y = Pos.Bottom (spacingOptions), + Width = Dim.Fill (), + Height = Dim.Fill (), + SchemeName = "Runnable", + Source = new ListWrapper (eventSource) + }; + configView.Add (eventLog); + + foreach (View view in mainWindow.SubViews.Where (v => v is LinearRange)!) + { + var slider = (LinearRange)view; + + slider.Accepting += (_, args) => + { + eventSource.Add ($"Accept: {string.Join (",", slider.GetSetOptions ())}"); + eventLog.MoveDown (); + args.Handled = true; + }; + + slider.OptionsChanged += (_, args) => + { + eventSource.Add ($"OptionsChanged: {string.Join (",", slider.GetSetOptions ())}"); + eventLog.MoveDown (); + args.Cancel = true; + }; + } + + mainWindow.FocusDeepest (NavigationDirection.Forward, null); + + app.Run (mainWindow); + + return; + + void OnSliderFgColorOnOptionsChanged (object _, LinearRangeEventArgs<(Color, Color)> e) + { + if (e.Options.Count == 0) + { + return; + } + + (Color, Color) data = e.Options.First ().Value.Data; + + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.SetScheme ( + new (s.GetScheme ()) + { + Normal = new ( + data.Item2, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style) + }); + + s.Style.OptionChar = s.Style.OptionChar with + { + Attribute = new Attribute ( + data.Item1, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style) + }; + + s.Style.SetChar = s.Style.SetChar with + { + Attribute = new Attribute ( + data.Item1, + s.Style.SetChar.Attribute?.Background + ?? s.GetAttributeForRole (VisualRole.Normal).Background, + s.Style.SetChar.Attribute?.Style + ?? s.GetAttributeForRole (VisualRole.Normal).Style) + }; + + s.Style.LegendAttributes.SetAttribute = new Attribute ( + data.Item1, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style); + + s.Style.RangeChar = s.Style.RangeChar with + { + Attribute = new Attribute ( + data.Item1, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style) + }; + + s.Style.SpaceChar = s.Style.SpaceChar with + { + Attribute = new Attribute ( + data.Item1, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style) + }; + + s.Style.LegendAttributes.NormalAttribute = new Attribute ( + data.Item1, + s.GetAttributeForRole (VisualRole.Normal).Background, + s.GetAttributeForRole (VisualRole.Normal).Style); + } + } + + void OnLegendsOrientationSliderOnOptionsChanged (object _, LinearRangeEventArgs e) + { + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + if (e.Options.ContainsKey (0)) + { + s.LegendsOrientation = Orientation.Horizontal; + } + else if (e.Options.ContainsKey (1)) + { + s.LegendsOrientation = Orientation.Vertical; + } + + if (optionsSlider.GetSetOptions ().Contains (3)) + { + s.Width = Dim.Auto (DimAutoStyle.Content); + s.Height = Dim.Auto (DimAutoStyle.Content); + } + else + { + if (s.Orientation == Orientation.Horizontal) + { + s.Width = Dim.Percent (50); + + int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical + ? s.Options.Max (o => o.Legend!.Length) + 3 + : 4; + s.Height = h; + } + else + { + int w = s.ShowLegends ? s.Options.Max (o => o.Legend!.Length) + 3 : 3; + s.Width = w; + s.Height = Dim.Fill (); + } + } + } + } + + void OnOrientationSliderOnOptionsChanged (object _, LinearRangeEventArgs e) + { + View prev = null; + + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + if (e.Options.ContainsKey (0)) + { + s.Orientation = Orientation.Horizontal; + + s.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; + + if (prev == null) + { + s.Y = 0; + } + else + { + s.Y = Pos.Bottom (prev) + 1; + } + + s.X = 0; + prev = s; + } + else if (e.Options.ContainsKey (1)) + { + s.Orientation = Orientation.Vertical; + + s.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; + + if (prev == null) + { + s.X = 0; + } + else + { + s.X = Pos.Right (prev) + 2; + } + + s.Y = 0; + prev = s; + } + + if (optionsSlider.GetSetOptions ().Contains (3)) + { + s.Width = Dim.Auto (DimAutoStyle.Content); + s.Height = Dim.Auto (DimAutoStyle.Content); + } + else + { + if (s.Orientation == Orientation.Horizontal) + { + s.Width = Dim.Percent (50); + + int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical + ? s.Options.Max (o => o.Legend!.Length) + 3 + : 4; + s.Height = h; + } + else + { + int w = s.ShowLegends ? s.Options.Max (o => o.Legend!.Length) + 3 : 3; + s.Width = w; + s.Height = Dim.Fill (); + } + } + } + } + + void OnOptionsSliderOnOptionsChanged (object _, LinearRangeEventArgs e) + { + foreach (LinearRange s in mainWindow.SubViews.OfType ()) + { + s.ShowLegends = e.Options.ContainsKey (0); + s.RangeAllowSingle = e.Options.ContainsKey (1); + s.ShowEndSpacing = e.Options.ContainsKey (2); + + if (e.Options.ContainsKey (3)) + { + s.Width = Dim.Auto (DimAutoStyle.Content); + s.Height = Dim.Auto (DimAutoStyle.Content); + } + else + { + if (s.Orientation == Orientation.Horizontal) + { + s.Width = Dim.Percent (50); + + int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical + ? s.Options.Max (o => o.Legend!.Length) + 3 + : 4; + s.Height = h; + } + else + { + int w = s.ShowLegends ? s.Options.Max (o => o.Legend!.Length) + 3 : 3; + s.Width = w; + s.Height = Dim.Fill (); + } + } + } + } + } + + private void MakeSliders (Window window, List options) + { + List types = Enum.GetValues (typeof (LinearRangeType)).Cast ().ToList (); + LinearRange prev = null; + + foreach (LinearRange view in types.Select (type => new LinearRange (options) + { + Title = type.ToString (), + X = 0, + Y = prev == null ? 0 : Pos.Bottom (prev), + BorderStyle = LineStyle.Single, + Type = type, + AllowEmpty = true + })) + { + //view.Padding.Thickness = new (0,1,0,0); + window.Add (view); + prev = view; + } + + List singleOptions = + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39 + ]; + + LinearRange single = new (singleOptions) + { + Title = "_Continuous", + X = 0, + Y = prev == null ? 0 : Pos.Bottom (prev), + Type = LinearRangeType.Single, + BorderStyle = LineStyle.Single, + AllowEmpty = false + }; + + single.SubViewLayout += (_, _) => + { + if (single.Orientation == Orientation.Horizontal) + { + single.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; + single.Style.OptionChar = new () { Grapheme = Glyphs.HLine.ToString () }; + } + else + { + single.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; + single.Style.OptionChar = new () { Grapheme = Glyphs.VLine.ToString () }; + } + }; + single.Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; + single.Style.DragChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; + + window.Add (single); + + single.OptionsChanged += (_, e) => { single.Title = $"_Continuous {e.Options.FirstOrDefault ().Key}"; }; + + List oneOption = new () { "The Only Option" }; + + LinearRange one = new (oneOption) + { + Title = "_One Option", + X = 0, + Y = prev == null ? 0 : Pos.Bottom (single), + Type = LinearRangeType.Single, + BorderStyle = LineStyle.Single, + AllowEmpty = false + }; + window.Add (one); + } +} diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 5a76edd12..1fd133a71 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -229,8 +229,8 @@ public class Shortcuts : Scenario X = 0, Y = Pos.Bottom (optionSelectorShortcut), Width = Dim.Fill ()! - Dim.Width (eventLog), - HelpText = "Sliders work!", - CommandView = new Slider + HelpText = "LinearRanges work!", + CommandView = new LinearRange { Orientation = Orientation.Horizontal, AllowEmpty = true @@ -238,13 +238,13 @@ public class Shortcuts : Scenario Key = Key.F5 }; - ((Slider)sliderShortcut.CommandView).Options = [new () { Legend = "A" }, new () { Legend = "B" }, new () { Legend = "C" }]; - ((Slider)sliderShortcut.CommandView).SetOption (0); + ((LinearRange)sliderShortcut.CommandView).Options = [new () { Legend = "A" }, new () { Legend = "B" }, new () { Legend = "C" }]; + ((LinearRange)sliderShortcut.CommandView).SetOption (0); - ((Slider)sliderShortcut.CommandView).OptionsChanged += (o, args) => + ((LinearRange)sliderShortcut.CommandView).OptionsChanged += (o, args) => { eventSource.Add ( - $"OptionsChanged: {o?.GetType ().Name} - {string.Join (",", ((Slider)o!)!.GetSetOptions ())}"); + $"OptionsChanged: {o?.GetType ().Name} - {string.Join (",", ((LinearRange)o!)!.GetSetOptions ())}"); eventLog.MoveDown (); }; diff --git a/Examples/UICatalog/Scenarios/Sliders.cs b/Examples/UICatalog/Scenarios/Sliders.cs deleted file mode 100644 index d23eae48d..000000000 --- a/Examples/UICatalog/Scenarios/Sliders.cs +++ /dev/null @@ -1,621 +0,0 @@ -using System.Collections.ObjectModel; -using System.Text; - -namespace UICatalog.Scenarios; - -[ScenarioMetadata ("Sliders", "Demonstrates the Slider view.")] -[ScenarioCategory ("Controls")] -public class Sliders : Scenario -{ - public void MakeSliders (View v, List options) - { - List types = Enum.GetValues (typeof (SliderType)).Cast ().ToList (); - Slider prev = null; - - foreach (SliderType type in types) - { - var view = new Slider (options) - { - Title = type.ToString (), - X = 0, - Y = prev == null ? 0 : Pos.Bottom (prev), - BorderStyle = LineStyle.Single, - Type = type, - AllowEmpty = true - }; - - //view.Padding.Thickness = new (0,1,0,0); - v.Add (view); - prev = view; - } - - List singleOptions = new () - { - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39 - }; - - var single = new Slider (singleOptions) - { - Title = "_Continuous", - X = 0, - Y = prev == null ? 0 : Pos.Bottom (prev), - Type = SliderType.Single, - BorderStyle = LineStyle.Single, - AllowEmpty = false - }; - - single.SubViewLayout += (s, e) => - { - if (single.Orientation == Orientation.Horizontal) - { - single.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; - single.Style.OptionChar = new () { Grapheme = Glyphs.HLine.ToString () }; - } - else - { - single.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; - single.Style.OptionChar = new () { Grapheme = Glyphs.VLine.ToString () }; - } - }; - single.Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; - single.Style.DragChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; - - v.Add (single); - - single.OptionsChanged += (s, e) => { single.Title = $"_Continuous {e.Options.FirstOrDefault ().Key}"; }; - - List oneOption = new () { "The Only Option" }; - - var one = new Slider (oneOption) - { - Title = "_One Option", - X = 0, - Y = prev == null ? 0 : Pos.Bottom (single), - Type = SliderType.Single, - BorderStyle = LineStyle.Single, - AllowEmpty = false - }; - v.Add (one); - } - - public override void Main () - { - Application.Init (); - - Window app = new () - { - Title = GetQuitKeyAndName () - }; - - MakeSliders ( - app, - new () - { - 500, - 1000, - 1500, - 2000, - 2500, - 3000, - 3500, - 4000, - 4500, - 5000 - } - ); - - var configView = new FrameView - { - Title = "Confi_guration", - X = Pos.Percent (50), - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (), - SchemeName = "Dialog" - }; - - app.Add (configView); - - #region Config Slider - - Slider optionsSlider = new () - { - Title = "Options", - X = 0, - Y = 0, - Width = Dim.Fill (), - Type = SliderType.Multiple, - AllowEmpty = true, - BorderStyle = LineStyle.Single - }; - - optionsSlider.Style.SetChar = optionsSlider.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; - optionsSlider.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Black); - - optionsSlider.Options = new () - { - new () { Legend = "Legends" }, - new () { Legend = "RangeAllowSingle" }, - new () { Legend = "EndSpacing" }, - new () { Legend = "DimAuto" } - }; - - configView.Add (optionsSlider); - - optionsSlider.OptionsChanged += (sender, e) => - { - foreach (Slider s in app.SubViews.OfType ()) - { - s.ShowLegends = e.Options.ContainsKey (0); - s.RangeAllowSingle = e.Options.ContainsKey (1); - s.ShowEndSpacing = e.Options.ContainsKey (2); - - if (e.Options.ContainsKey (3)) - { - s.Width = Dim.Auto (DimAutoStyle.Content); - s.Height = Dim.Auto (DimAutoStyle.Content); - } - else - { - if (s.Orientation == Orientation.Horizontal) - { - s.Width = Dim.Percent (50); - - int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical - ? s.Options.Max (o => o.Legend.Length) + 3 - : 4; - s.Height = h; - } - else - { - int w = s.ShowLegends ? s.Options.Max (o => o.Legend.Length) + 3 : 3; - s.Width = w; - s.Height = Dim.Fill (); - } - } - } - }; - optionsSlider.SetOption (0); // Legends - optionsSlider.SetOption (1); // RangeAllowSingle - optionsSlider.SetOption (3); // DimAuto - - CheckBox dimAutoUsesMin = new () - { - Text = "Use minimum size (vs. ideal)", - X = 0, - Y = Pos.Bottom (optionsSlider) - }; - - dimAutoUsesMin.CheckedStateChanging += (sender, e) => - { - foreach (Slider s in app.SubViews.OfType ()) - { - s.UseMinimumSize = !s.UseMinimumSize; - } - }; - configView.Add (dimAutoUsesMin); - - #region Slider Orientation Slider - - Slider orientationSlider = new (new () { "Horizontal", "Vertical" }) - { - Title = "Slider Orientation", - X = 0, - Y = Pos.Bottom (dimAutoUsesMin) + 1, - BorderStyle = LineStyle.Single - }; - - orientationSlider.SetOption (0); - - configView.Add (orientationSlider); - - orientationSlider.OptionsChanged += (sender, e) => - { - View prev = null; - - foreach (Slider s in app.SubViews.OfType ()) - { - if (e.Options.ContainsKey (0)) - { - s.Orientation = Orientation.Horizontal; - - s.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; - - if (prev == null) - { - s.Y = 0; - } - else - { - s.Y = Pos.Bottom (prev) + 1; - } - - s.X = 0; - prev = s; - } - else if (e.Options.ContainsKey (1)) - { - s.Orientation = Orientation.Vertical; - - s.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () }; - - if (prev == null) - { - s.X = 0; - } - else - { - s.X = Pos.Right (prev) + 2; - } - - s.Y = 0; - prev = s; - } - - if (optionsSlider.GetSetOptions ().Contains (3)) - { - s.Width = Dim.Auto (DimAutoStyle.Content); - s.Height = Dim.Auto (DimAutoStyle.Content); - } - else - { - if (s.Orientation == Orientation.Horizontal) - { - s.Width = Dim.Percent (50); - - int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical - ? s.Options.Max (o => o.Legend.Length) + 3 - : 4; - s.Height = h; - } - else - { - int w = s.ShowLegends ? s.Options.Max (o => o.Legend.Length) + 3 : 3; - s.Width = w; - s.Height = Dim.Fill (); - } - } - } - }; - - #endregion Slider Orientation Slider - - #region Legends Orientation Slider - - Slider legendsOrientationSlider = new (new () { "Horizontal", "Vertical" }) - { - Title = "Legends Orientation", - X = 0, - Y = Pos.Bottom (orientationSlider) + 1, - BorderStyle = LineStyle.Single - }; - - legendsOrientationSlider.SetOption (0); - - configView.Add (legendsOrientationSlider); - - legendsOrientationSlider.OptionsChanged += (sender, e) => - { - foreach (Slider s in app.SubViews.OfType ()) - { - if (e.Options.ContainsKey (0)) - { - s.LegendsOrientation = Orientation.Horizontal; - } - else if (e.Options.ContainsKey (1)) - { - s.LegendsOrientation = Orientation.Vertical; - } - - if (optionsSlider.GetSetOptions ().Contains (3)) - { - s.Width = Dim.Auto (DimAutoStyle.Content); - s.Height = Dim.Auto (DimAutoStyle.Content); - } - else - { - if (s.Orientation == Orientation.Horizontal) - { - s.Width = Dim.Percent (50); - - int h = s.ShowLegends && s.LegendsOrientation == Orientation.Vertical - ? s.Options.Max (o => o.Legend.Length) + 3 - : 4; - s.Height = h; - } - else - { - int w = s.ShowLegends ? s.Options.Max (o => o.Legend.Length) + 3 : 3; - s.Width = w; - s.Height = Dim.Fill (); - } - } - } - }; - - #endregion Legends Orientation Slider - - #region Spacing Options - - FrameView spacingOptions = new () - { - Title = "Spacing Options", - X = Pos.Right (orientationSlider), - Y = Pos.Top (orientationSlider), - Width = Dim.Fill (), - Height = Dim.Auto (), - BorderStyle = LineStyle.Single - }; - - Label label = new () - { - Text = "Min _Inner Spacing:" - }; - - NumericUpDown innerSpacingUpDown = new () - { - X = Pos.Right (label) + 1 - }; - - innerSpacingUpDown.Value = app.SubViews.OfType ().First ().MinimumInnerSpacing; - - innerSpacingUpDown.ValueChanging += (sender, e) => - { - if (e.NewValue < 0) - { - e.Cancel = true; - - return; - } - - foreach (Slider s in app.SubViews.OfType ()) - { - s.MinimumInnerSpacing = e.NewValue; - } - }; - - spacingOptions.Add (label, innerSpacingUpDown); - configView.Add (spacingOptions); - - #endregion - - #region Color Slider - - foreach (Slider s in app.SubViews.OfType ()) - { - s.Style.OptionChar = s.Style.OptionChar with { Attribute = app.GetAttributeForRole (VisualRole.Normal) }; - s.Style.SetChar = s.Style.SetChar with { Attribute = app.GetAttributeForRole (VisualRole.Normal) }; - s.Style.LegendAttributes.SetAttribute = app.GetAttributeForRole (VisualRole.Normal); - s.Style.RangeChar = s.Style.RangeChar with { Attribute = app.GetAttributeForRole (VisualRole.Normal) }; - } - - Slider<(Color, Color)> sliderFGColor = new () - { - Title = "FG Color", - X = 0, - Y = Pos.Bottom ( - legendsOrientationSlider - ) - + 1, - Type = SliderType.Single, - BorderStyle = LineStyle.Single, - AllowEmpty = false, - Orientation = Orientation.Vertical, - LegendsOrientation = Orientation.Horizontal, - MinimumInnerSpacing = 0, - UseMinimumSize = true - }; - - sliderFGColor.Style.SetChar = sliderFGColor.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; - sliderFGColor.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Blue); - - List> colorOptions = new (); - - foreach (ColorName16 colorIndex in Enum.GetValues ()) - { - var colorName = colorIndex.ToString (); - - colorOptions.Add ( - new () - { - Data = (new (colorIndex), - new (colorIndex)), - Legend = colorName, - LegendAbbr = (Rune)colorName [0] - } - ); - } - - sliderFGColor.Options = colorOptions; - - configView.Add (sliderFGColor); - - sliderFGColor.OptionsChanged += (sender, e) => - { - if (e.Options.Count != 0) - { - (Color, Color) data = e.Options.First ().Value.Data; - - foreach (Slider s in app.SubViews.OfType ()) - { - s.SetScheme ( - new (s.GetScheme ()) - { - Normal = new ( - data.Item2, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style - ) - }); - - s.Style.OptionChar = s.Style.OptionChar with - { - Attribute = new Attribute ( - data.Item1, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style - ) - }; - - s.Style.SetChar = s.Style.SetChar with - { - Attribute = new Attribute ( - data.Item1, - s.Style.SetChar.Attribute?.Background - ?? s.GetAttributeForRole (VisualRole.Normal).Background, - s.Style.SetChar.Attribute?.Style - ?? s.GetAttributeForRole (VisualRole.Normal).Style - ) - }; - - s.Style.LegendAttributes.SetAttribute = - new Attribute ( - data.Item1, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style); - - s.Style.RangeChar = s.Style.RangeChar with - { - Attribute = new Attribute ( - data.Item1, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style) - }; - - s.Style.SpaceChar = s.Style.SpaceChar with - { - Attribute = new Attribute ( - data.Item1, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style) - }; - - s.Style.LegendAttributes.NormalAttribute = - new Attribute ( - data.Item1, - s.GetAttributeForRole (VisualRole.Normal).Background, - s.GetAttributeForRole (VisualRole.Normal).Style); - } - } - }; - - Slider<(Color, Color)> sliderBGColor = new () - { - Title = "BG Color", - X = Pos.Right (sliderFGColor), - Y = Pos.Top (sliderFGColor), - Type = SliderType.Single, - BorderStyle = LineStyle.Single, - AllowEmpty = false, - Orientation = Orientation.Vertical, - LegendsOrientation = Orientation.Horizontal, - MinimumInnerSpacing = 0, - UseMinimumSize = true - }; - - sliderBGColor.Style.SetChar = sliderBGColor.Style.SetChar with { Attribute = new Attribute (Color.BrightGreen, Color.Black) }; - sliderBGColor.Style.LegendAttributes.SetAttribute = new Attribute (Color.Green, Color.Blue); - - sliderBGColor.Options = colorOptions; - - configView.Add (sliderBGColor); - - sliderBGColor.OptionsChanged += (sender, e) => - { - if (e.Options.Count != 0) - { - (Color, Color) data = e.Options.First ().Value.Data; - - foreach (Slider s in app.SubViews.OfType ()) - { - s.SetScheme ( - new (s.GetScheme ()) - { - Normal = new ( - s.GetAttributeForRole (VisualRole.Normal).Foreground, - data.Item2 - ) - }); - } - } - }; - - #endregion Color Slider - - #endregion Config Slider - - ObservableCollection eventSource = new (); - - var eventLog = new ListView - { - X = Pos.Right (sliderBGColor), - Y = Pos.Bottom (spacingOptions), - Width = Dim.Fill (), - Height = Dim.Fill (), - SchemeName = "Runnable", - Source = new ListWrapper (eventSource) - }; - configView.Add (eventLog); - - foreach (Slider slider in app.SubViews.Where (v => v is Slider)!) - { - slider.Accepting += (o, args) => - { - eventSource.Add ($"Accept: {string.Join (",", slider.GetSetOptions ())}"); - eventLog.MoveDown (); - args.Handled = true; - }; - - slider.OptionsChanged += (o, args) => - { - eventSource.Add ($"OptionsChanged: {string.Join (",", slider.GetSetOptions ())}"); - eventLog.MoveDown (); - args.Cancel = true; - }; - } - - app.FocusDeepest (NavigationDirection.Forward, null); - - Application.Run (app); - app.Dispose (); - Application.Shutdown (); - } -} diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index 4edd90d0c..8c93b6b07 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -201,17 +201,17 @@ public class ViewportSettings : Scenario List options = new () { "Option 1", "Option 2", "Option 3" }; - Slider slider = new (options) + LinearRange linearRange = new (options) { X = 0, Y = Pos.Bottom (textField) + 1, Orientation = Orientation.Vertical, - Type = SliderType.Multiple, + Type = LinearRangeType.Multiple, AllowEmpty = false, BorderStyle = LineStyle.Double, - Title = "_Slider" + Title = "_LinearRange" }; - view.Add (slider); + view.Add (linearRange); adornmentsEditor.Initialized += (s, e) => { diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/LinearRange/LinearRange.cs similarity index 63% rename from Terminal.Gui/Views/Slider/Slider.cs rename to Terminal.Gui/Views/LinearRange/LinearRange.cs index 5c7a9f347..3b5c7b74c 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRange.cs @@ -1,46 +1,44 @@ -#nullable disable -using System.Transactions; - namespace Terminal.Gui.Views; /// -/// Provides a slider control letting the user navigate from a set of typed options in a linear manner using the +/// Provides a linear range control letting the user navigate from a set of typed options in a linear manner using the /// keyboard or mouse. /// -public class Slider : Slider +public class LinearRange : LinearRange { - /// Initializes a new instance of the class. - public Slider () { } + /// Initializes a new instance of the class. + public LinearRange () { } - /// Initializes a new instance of the class. - /// Initial slider options. - /// Initial slider options. - public Slider (List options, Orientation orientation = Orientation.Horizontal) : + /// Initializes a new instance of the class. + /// Initial options. + /// Initial orientation. + public LinearRange (List options, Orientation orientation = Orientation.Horizontal) : base (options, orientation) { } } /// -/// Provides a tpe-safe slider control letting the user navigate from a set of typed options in a linear manner using the +/// Provides a type-safe linear range control letting the user navigate from a set of typed options in a linear manner +/// using the /// keyboard or mouse. /// /// -public class Slider : View, IOrientation +public class LinearRange : View, IOrientation { - private readonly SliderConfiguration _config = new (); + private readonly LinearRangeConfiguration _config = new (); // List of the current set options. - private readonly List _setOptions = new (); + private readonly List _setOptions = []; // Options - private List> _options; + private List>? _options; - private OrientationHelper _orientationHelper; + private OrientationHelper? _orientationHelper; #region Initialize private void SetInitialProperties ( - List> options, + List> options, Orientation orientation = Orientation.Horizontal ) { @@ -49,10 +47,10 @@ public class Slider : View, IOrientation CanFocus = true; CursorVisibility = CursorVisibility.Default; - _options = options ?? new List> (); + _options = options; _orientationHelper = new (this); // Do not use object initializer! - _orientationHelper.Orientation = _config._sliderOrientation = orientation; + _orientationHelper.Orientation = _config._linearRangeOrientation = orientation; _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); @@ -60,9 +58,6 @@ public class Slider : View, IOrientation SetCommands (); SetContentSize (); - // BUGBUG: This should not be needed - Need to ensure SetRelativeLayout gets called during EndInit - Initialized += (s, e) => { SetContentSize (); }; - SubViewLayout += (s, e) => { SetContentSize (); }; } @@ -71,7 +66,7 @@ public class Slider : View, IOrientation { _config._showLegends = true; - switch (_config._sliderOrientation) + switch (_config._linearRangeOrientation) { case Orientation.Horizontal: Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; // '─' @@ -85,63 +80,44 @@ public class Slider : View, IOrientation break; } - // TODO(jmperricone) Wide Vertical ??? - /* - │ - │ - ┼─ 40 - │ - │ - ███ 30 - ▒▒▒ - ▒▒▒ - ▒▒▒ 20 - ▒▒▒ - ▒▒▒ - ███ 10 - │ - │ - ─●─ 0 - */ - - _config._legendsOrientation = _config._sliderOrientation; + _config._legendsOrientation = _config._linearRangeOrientation; Style.EmptyChar = new () { Grapheme = " " }; Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; // ■ Style.RangeChar = new () { Grapheme = Glyphs.Stipple.ToString () }; // ░ ▒ ▓ // Medium shade not blinking on curses. Style.StartRangeChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; Style.EndRangeChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () }; Style.DragChar = new () { Grapheme = Glyphs.Diamond.ToString () }; - - // TODO: Support left & right (top/bottom) - // First = '├', - // Last = '┤', } #endregion #region Constructors - /// Initializes a new instance of the class. - public Slider () : this (new ()) { } + /// Initializes a new instance of the class. + public LinearRange () : this (new ()) { } - /// Initializes a new instance of the class. - /// Initial slider options. - /// Initial slider orientation. - public Slider (List options, Orientation orientation = Orientation.Horizontal) + /// Initializes a new instance of the class. + /// Initial options. + /// Initial orientation. + public LinearRange (List? options, Orientation orientation = Orientation.Horizontal) { if (options is null) { - SetInitialProperties (null, orientation); + return; + } + + if (options is { Count: 0 }) + { + SetInitialProperties ([], orientation); } else { SetInitialProperties ( - options.Select ( - e => + options.Select (e => { - var legend = e.ToString (); + string? legend = e?.ToString (); - return new SliderOption + return new LinearRangeOption { Data = e, Legend = legend, @@ -161,20 +137,13 @@ public class Slider : View, IOrientation #region Properties /// - /// Setting the Text of a slider is a shortcut to setting options. The text is a CSV string of the options. + /// Setting the Text of a linear range is a shortcut to setting options. The text is a CSV string of the options. /// public override string Text { - get - { - if (_options.Count == 0) - { - return string.Empty; - } - - // Return labels as a CSV string - return string.Join (",", _options); - } + // Return labels as a CSV string + get => _options is null or { Count: 0 } ? string.Empty : + string.Join (",", _options); set { if (string.IsNullOrEmpty (value)) @@ -184,7 +153,7 @@ public class Slider : View, IOrientation else { IEnumerable list = value.Split (',').Select (x => x.Trim ()); - Options = list.Select (x => new SliderOption { Legend = x }).ToList (); + Options = list.Select (x => new LinearRangeOption { Legend = x }).ToList (); } } } @@ -197,7 +166,7 @@ public class Slider : View, IOrientation { _config._allowEmpty = value; - if (!value && _options.Count > 0 && _setOptions.Count == 0) + if (!value && _options!.Count > 0 && _setOptions.Count == 0) { SetOption (0); } @@ -210,50 +179,102 @@ public class Slider : View, IOrientation get => _config._minInnerSpacing; set { - _config._minInnerSpacing = value; + int current = _config._minInnerSpacing; - SetContentSize (); + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnMinimumInnerSpacingChanging, + MinimumInnerSpacingChanging, + newValue => + { + _config._minInnerSpacing = newValue; + SetContentSize (); + }, + OnMinimumInnerSpacingChanged, + MinimumInnerSpacingChanged, + out int _ + ); } } - /// Slider Type. - public SliderType Type + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? MinimumInnerSpacingChanging; + + /// Event raised after the property has changed. + public event EventHandler>? MinimumInnerSpacingChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnMinimumInnerSpacingChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnMinimumInnerSpacingChanged (ValueChangedEventArgs args) { } + + /// LinearRange Type. + public LinearRangeType Type { get => _config._type; set { - _config._type = value; + LinearRangeType current = _config._type; - // Todo: Custom logic to preserve options. - _setOptions.Clear (); - SetNeedsDraw (); + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnTypeChanging, + TypeChanging, + newValue => + { + _config._type = newValue; + + // Todo: Custom logic to preserve options. + _setOptions.Clear (); + SetNeedsDraw (); + }, + OnTypeChanged, + TypeChanged, + out LinearRangeType _ + ); } } + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? TypeChanging; + + /// Event raised after the property has changed. + public event EventHandler>? TypeChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnTypeChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnTypeChanged (ValueChangedEventArgs args) { } /// /// Gets or sets the . The default is . /// public Orientation Orientation { - get => _orientationHelper.Orientation; - set => _orientationHelper.Orientation = value; + get => _orientationHelper!.Orientation; + set => _orientationHelper!.Orientation = value; } #region IOrientation members - /// - public event EventHandler> OrientationChanging; + /// + public event EventHandler>? OrientationChanging; - /// - public event EventHandler> OrientationChanged; + /// + public event EventHandler>? OrientationChanged; - /// + /// public void OnOrientationChanged (Orientation newOrientation) { - _config._sliderOrientation = newOrientation; + _config._linearRangeOrientation = newOrientation; - switch (_config._sliderOrientation) + switch (_config._linearRangeOrientation) { case Orientation.Horizontal: Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () }; // '─' @@ -268,6 +289,7 @@ public class Slider : View, IOrientation SetKeyBindings (); SetContentSize (); } + #endregion /// Legends Orientation. @@ -276,20 +298,45 @@ public class Slider : View, IOrientation get => _config._legendsOrientation; set { - _config._legendsOrientation = value; + Orientation current = _config._legendsOrientation; - SetContentSize (); + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnLegendsOrientationChanging, + LegendsOrientationChanging, + newValue => + { + _config._legendsOrientation = newValue; + SetContentSize (); + }, + OnLegendsOrientationChanged, + LegendsOrientationChanged, + out Orientation _ + ); } } - /// Slider styles. - public SliderStyle Style { get; set; } = new (); + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? LegendsOrientationChanging; - /// Set the slider options. - public List> Options + /// Event raised after the property has changed. + public event EventHandler>? LegendsOrientationChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnLegendsOrientationChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnLegendsOrientationChanged (ValueChangedEventArgs args) { } + + /// LinearRange styles. + public LinearRangeStyle Style { get; set; } = new (); + + /// Set the linear range options. + public List> Options { - get => - _options; + get => _options ?? []; set { // _options should never be null @@ -317,43 +364,127 @@ public class Slider : View, IOrientation get => _config._showEndSpacing; set { - _config._showEndSpacing = value; - SetContentSize (); + bool current = _config._showEndSpacing; + + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnShowEndSpacingChanging, + ShowEndSpacingChanging, + newValue => + { + _config._showEndSpacing = newValue; + SetContentSize (); + }, + OnShowEndSpacingChanged, + ShowEndSpacingChanged, + out bool _ + ); } } + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? ShowEndSpacingChanging; + + /// Event raised after the property has changed. + public event EventHandler>? ShowEndSpacingChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnShowEndSpacingChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnShowEndSpacingChanged (ValueChangedEventArgs args) { } + /// Show/Hide the options legends. public bool ShowLegends { get => _config._showLegends; set { - _config._showLegends = value; - SetContentSize (); + bool current = _config._showLegends; + + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnShowLegendsChanging, + ShowLegendsChanging, + newValue => + { + _config._showLegends = newValue; + SetContentSize (); + }, + OnShowLegendsChanged, + ShowLegendsChanged, + out bool _ + ); } } + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? ShowLegendsChanging; + + /// Event raised after the property has changed. + public event EventHandler>? ShowLegendsChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnShowLegendsChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnShowLegendsChanged (ValueChangedEventArgs args) { } + /// - /// Gets or sets whether the minimum or ideal size will be used when calculating the size of the slider. + /// Gets or sets whether the minimum or ideal size will be used when calculating the size of the linear range. /// public bool UseMinimumSize { get => _config._useMinimumSize; set { - _config._useMinimumSize = value; - SetContentSize (); + bool current = _config._useMinimumSize; + + CWPPropertyHelper.ChangeProperty ( + this, + ref current, + value, + OnUseMinimumSizeChanging, + UseMinimumSizeChanging, + newValue => + { + _config._useMinimumSize = newValue; + SetContentSize (); + }, + OnUseMinimumSizeChanged, + UseMinimumSizeChanged, + out bool _ + ); } } + /// Event raised before the property changes. Can be cancelled. + public event EventHandler>? UseMinimumSizeChanging; + + /// Event raised after the property has changed. + public event EventHandler>? UseMinimumSizeChanged; + + /// Called before changes. Return true to cancel the change. + protected virtual bool OnUseMinimumSizeChanging (ValueChangingEventArgs args) => false; + + /// Called after has changed. + protected virtual void OnUseMinimumSizeChanged (ValueChangedEventArgs args) { } + #endregion #region Events - /// Event raised when the slider option/s changed. The dictionary contains: key = option index, value = T - public event EventHandler> OptionsChanged; + /// Event raised when the linear range option/s changed. The dictionary contains: key = option index, value = T + public event EventHandler>? OptionsChanged; - /// Overridable method called when the slider options have changed. Raises the event. + /// + /// Overridable method called when the linear range options have changed. Raises the + /// event. + /// public virtual void OnOptionsChanged () { OptionsChanged?.Invoke (this, new (GetSetOptionDictionary ())); @@ -361,7 +492,7 @@ public class Slider : View, IOrientation } /// Event raised When the option is hovered with the keys or the mouse. - public event EventHandler> OptionFocused; + public event EventHandler>? OptionFocused; private int _lastFocusedOption; // for Range type; the most recently focused option. Used to determine shrink direction @@ -370,9 +501,9 @@ public class Slider : View, IOrientation /// /// if the focus change was cancelled. /// - public virtual bool OnOptionFocused (int newFocusedOption, SliderEventArgs args) + public virtual bool OnOptionFocused (int newFocusedOption, LinearRangeEventArgs args) { - if (newFocusedOption > _options.Count - 1 || newFocusedOption < 0) + if (newFocusedOption > _options!.Count - 1 || newFocusedOption < 0) { return true; } @@ -383,8 +514,6 @@ public class Slider : View, IOrientation { _lastFocusedOption = FocusedOption; FocusedOption = newFocusedOption; - - //PositionCursor (); } return args.Cancel; @@ -403,7 +532,7 @@ public class Slider : View, IOrientation // TODO: Handle range type. // Note: Maybe return false only when optionIndex doesn't exist, otherwise true. - if (!_setOptions.Contains (optionIndex) && optionIndex >= 0 && optionIndex < _options.Count) + if (!_setOptions.Contains (optionIndex) && optionIndex >= 0 && optionIndex < _options!.Count) { FocusedOption = optionIndex; SetFocusedOption (); @@ -451,15 +580,15 @@ public class Slider : View, IOrientation AddStr (str); } - /// Sets the dimensions of the Slider to the ideal values. + /// Sets the dimensions of the LinearRange to the ideal values. private void SetContentSize () { - if (_options.Count == 0) + if (_options is { Count: 0 }) { return; } - bool horizontal = _config._sliderOrientation == Orientation.Horizontal; + bool horizontal = _config._linearRangeOrientation == Orientation.Horizontal; if (UseMinimumSize) { @@ -482,29 +611,29 @@ public class Slider : View, IOrientation int maxLegend; // Because the legends are centered, the longest one determines inner spacing - if (_config._sliderOrientation == _config._legendsOrientation) + if (_config._linearRangeOrientation == _config._legendsOrientation) { - maxLegend = int.Max (_options.Max (s => s.Legend?.GetColumns () ?? 1), 1); + maxLegend = int.Max (_options!.Max (s => s.Legend?.GetColumns () ?? 1), 1); } else { maxLegend = 1; } - int minSizeThatFitsLegends = _options.Count == 1 ? maxLegend : _options.Sum (o => o.Legend.GetColumns ()); + int minSizeThatFitsLegends = _options!.Count == 1 ? maxLegend : _options.Sum (o => o.Legend!.GetColumns ()); - string first; - string last; + string? first; + string? last; + + _config._showLegendsAbbr = false; if (minSizeThatFitsLegends > size) { - _config._showLegendsAbbr = false; - - if (_config._sliderOrientation == _config._legendsOrientation) + if (_config._linearRangeOrientation == _config._legendsOrientation) { _config._showLegendsAbbr = true; - foreach (SliderOption o in _options.Where (op => op.LegendAbbr == default (Rune))) + foreach (LinearRangeOption o in _options.Where (op => op.LegendAbbr == default (Rune))) { o.LegendAbbr = (Rune)(o.Legend?.GetColumns () > 0 ? o.Legend [0] : ' '); } @@ -515,7 +644,6 @@ public class Slider : View, IOrientation } else { - _config._showLegendsAbbr = false; first = _options.First ().Legend; last = _options.Last ().Legend; } @@ -524,10 +652,10 @@ public class Slider : View, IOrientation // Hello // Left = He // Right = lo - int firstLeft = (first.Length - 1) / 2; // Chars count of the first option to the left. - int lastRight = last.Length / 2; // Chars count of the last option to the right. + int firstLeft = (first!.Length - 1) / 2; // Chars count of the first option to the left. + int lastRight = last!.Length / 2; // Chars count of the last option to the right. - if (_config._sliderOrientation != _config._legendsOrientation) + if (_config._linearRangeOrientation != _config._legendsOrientation) { firstLeft = 0; lastRight = 0; @@ -557,21 +685,22 @@ public class Slider : View, IOrientation /// private int CalcMinLength () { - if (_options.Count == 0) + if (_options is { Count: 0 }) { return 0; } var length = 0; length += _config._startSpacing + _config._endSpacing; - length += _options.Count; + length += _options!.Count; length += (_options.Count - 1) * _config._minInnerSpacing; return length; } /// - /// Gets the ideal width of the slider. The ideal width is the minimum width required to display all options and inner + /// Gets the ideal width of the linear range. The ideal width is the minimum width required to display all options and + /// inner /// spacing. /// /// @@ -586,7 +715,8 @@ public class Slider : View, IOrientation } /// - /// Gets the ideal height of the slider. The ideal height is the minimum height required to display all options and + /// Gets the ideal height of the linear range. The ideal height is the minimum height required to display all options + /// and /// inner spacing. /// /// @@ -607,58 +737,53 @@ public class Slider : View, IOrientation /// private int CalcIdealLength () { - if (_options.Count == 0) + if (_options is { Count: 0 }) { return 0; } - bool isVertical = Orientation == Orientation.Vertical; var length = 0; - if (_config._showLegends) + if (!_config._showLegends) { - if (_config._legendsOrientation == _config._sliderOrientation && _options.Count > 0) - { - // Each legend should be centered in a space the width of the longest legend, with one space between. - // Calculate the total length required for all legends. - //if (!isVertical) - { - int maxLegend = int.Max (_options.Max (s => s.Legend?.GetColumns () ?? 1), 1); - length = maxLegend * _options.Count + (_options.Count - 1); - } + return Math.Max (length, CalcMinLength ()); + } - // - //{ - // length = CalcMinLength (); - //} - } - else - { - length = CalcMinLength (); - } + if (_config._legendsOrientation == _config._linearRangeOrientation && _options!.Count > 0) + { + // Each legend should be centered in a space the width of the longest legend, with one space between. + // Calculate the total length required for all legends. + int maxLegend = int.Max (_options.Max (s => s.Legend?.GetColumns () ?? 1), 1); + length = maxLegend * _options.Count + (_options.Count - 1); + } + else + { + length = CalcMinLength (); } return Math.Max (length, CalcMinLength ()); } /// - /// Calculates the minimum dimension required for the slider and legends. + /// Calculates the minimum dimension required for the linear range and legends. /// /// private int CalcIdealThickness () { - var thickness = 1; // Always show the slider. + var thickness = 1; // Always show the linear range. - if (_config._showLegends) + if (!_config._showLegends) { - if (_config._legendsOrientation != _config._sliderOrientation && _options.Count > 0) - { - thickness += _options.Max (s => s.Legend?.GetColumns () ?? 0); - } - else - { - thickness += 1; - } + return thickness; + } + + if (_config._legendsOrientation != _config._linearRangeOrientation && _options!.Count > 0) + { + thickness += _options.Max (s => s.Legend?.GetColumns () ?? 0); + } + else + { + thickness += 1; } return thickness; @@ -672,7 +797,7 @@ public class Slider : View, IOrientation { position = (-1, -1); - if (option < 0 || option >= _options.Count ()) + if (option < 0 || option >= _options!.Count ()) { return false; } @@ -681,7 +806,7 @@ public class Slider : View, IOrientation offset += _config._startSpacing; offset += option * (_config._cachedInnerSpacing + 1); - if (_config._sliderOrientation == Orientation.Vertical) + if (_config._linearRangeOrientation == Orientation.Vertical) { position = (0, offset); } @@ -701,7 +826,6 @@ public class Slider : View, IOrientation /// internal bool TryGetOptionByPosition (int x, int y, int threshold, out int optionIdx) { - // Fix(jmperricone): Not working. optionIdx = -1; if (Orientation == Orientation.Horizontal) @@ -719,7 +843,7 @@ public class Slider : View, IOrientation int option = cx / (_config._cachedInnerSpacing + 1); bool valid = cx % (_config._cachedInnerSpacing + 1) == 0; - if (!valid || option < 0 || option > _options.Count - 1) + if (!valid || option < 0 || option > _options!.Count - 1) { continue; } @@ -744,7 +868,7 @@ public class Slider : View, IOrientation int option = cy / (_config._cachedInnerSpacing + 1); bool valid = cy % (_config._cachedInnerSpacing + 1) == 0; - if (!valid || option < 0 || option > _options.Count - 1) + if (!valid || option < 0 || option > _options!.Count - 1) { continue; } @@ -761,17 +885,17 @@ public class Slider : View, IOrientation /// public override Point? PositionCursor () { - if (TryGetPositionByOption (FocusedOption, out (int x, int y) position)) + if (!TryGetPositionByOption (FocusedOption, out (int x, int y) position) + || !IsInitialized + || !Viewport.Contains (position.x, position.y)) { - if (IsInitialized && Viewport.Contains (position.x, position.y)) - { - Move (position.x, position.y); - - return new (position.x, position.y); - } + return base.PositionCursor (); } - return base.PositionCursor (); + Move (position.x, position.y); + + return new (position.x, position.y); + } #endregion Cursor and Position @@ -779,7 +903,7 @@ public class Slider : View, IOrientation #region Drawing /// - protected override bool OnDrawingContent (DrawContext context) + protected override bool OnDrawingContent (DrawContext? context) { // TODO: make this more surgical to reduce repaint @@ -788,8 +912,8 @@ public class Slider : View, IOrientation return true; } - // Draw Slider - DrawSlider (); + // Draw LinearRange + DrawLinearRange (); // Draw Legends. if (_config._showLegends) @@ -805,9 +929,9 @@ public class Slider : View, IOrientation return true; } - private string AlignText (string text, int width, Alignment alignment) + private static string AlignText (string? text, int width, Alignment alignment) { - if (text is null) + if (string.IsNullOrEmpty (text)) { return ""; } @@ -842,13 +966,12 @@ public class Slider : View, IOrientation } } - private void DrawSlider () + private void DrawLinearRange () { // TODO: be more surgical on clear - ClearViewport (null); + ClearViewport (); // Attributes - var normalAttr = new Attribute (Color.White, Color.Black); var setAttr = new Attribute (Color.Black, Color.White); @@ -858,9 +981,7 @@ public class Slider : View, IOrientation setAttr = Style.SetChar.Attribute ?? GetAttributeForRole (VisualRole.HotNormal); } - bool isVertical = _config._sliderOrientation == Orientation.Vertical; - bool isLegendsVertical = _config._legendsOrientation == Orientation.Vertical; - bool isReverse = _config._sliderOrientation != _config._legendsOrientation; + bool isVertical = _config._linearRangeOrientation == Orientation.Vertical; var x = 0; var y = 0; @@ -868,14 +989,14 @@ public class Slider : View, IOrientation bool isSet = _setOptions.Count > 0; // Left Spacing - if (_config._showEndSpacing && _config._startSpacing > 0) + if (_config is { _showEndSpacing: true, _startSpacing: > 0 }) { SetAttribute ( - isSet && _config._type == SliderType.LeftRange - ? Style.RangeChar.Attribute ?? normalAttr - : Style.SpaceChar.Attribute ?? normalAttr - ); - string text = isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; + isSet && _config._type == LinearRangeType.LeftRange + ? Style.RangeChar.Attribute ?? normalAttr + : Style.SpaceChar.Attribute ?? normalAttr + ); + string text = isSet && _config._type == LinearRangeType.LeftRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; for (var i = 0; i < _config._startSpacing; i++) { @@ -911,7 +1032,7 @@ public class Slider : View, IOrientation } // Slider - if (_options.Count > 0) + if (_options!.Count > 0) { for (var i = 0; i < _options.Count; i++) { @@ -921,19 +1042,19 @@ public class Slider : View, IOrientation { switch (_config._type) { - case SliderType.LeftRange when i <= _setOptions [0]: + case LinearRangeType.LeftRange when i <= _setOptions [0]: drawRange = i < _setOptions [0]; break; - case SliderType.RightRange when i >= _setOptions [0]: + case LinearRangeType.RightRange when i >= _setOptions [0]: drawRange = i >= _setOptions [0]; break; - case SliderType.Range when _setOptions.Count == 1: + case LinearRangeType.Range when _setOptions.Count == 1: drawRange = false; break; - case SliderType.Range when _setOptions.Count == 2: + case LinearRangeType.Range when _setOptions.Count == 2: if ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0])) { @@ -947,9 +1068,9 @@ public class Slider : View, IOrientation // Draw Option SetAttribute ( - isSet && _setOptions.Contains (i) ? Style.SetChar.Attribute ?? setAttr : - drawRange ? Style.RangeChar.Attribute ?? setAttr : Style.OptionChar.Attribute ?? normalAttr - ); + isSet && _setOptions.Contains (i) ? Style.SetChar.Attribute ?? setAttr : + drawRange ? Style.RangeChar.Attribute ?? setAttr : Style.OptionChar.Attribute ?? normalAttr + ); string text = drawRange ? Style.RangeChar.Grapheme : Style.OptionChar.Grapheme; @@ -981,27 +1102,29 @@ public class Slider : View, IOrientation } // Draw Spacing - if (_config._showEndSpacing || i < _options.Count - 1) + if (!_config._showEndSpacing && i >= _options.Count - 1) { - // Skip if is the Last Spacing. - SetAttribute ( - drawRange && isSet - ? Style.RangeChar.Attribute ?? setAttr - : Style.SpaceChar.Attribute ?? normalAttr - ); + continue; + } - for (var s = 0; s < _config._cachedInnerSpacing; s++) + // Skip if is the Last Spacing. + SetAttribute ( + drawRange && isSet + ? Style.RangeChar.Attribute ?? setAttr + : Style.SpaceChar.Attribute ?? normalAttr + ); + + for (var s = 0; s < _config._cachedInnerSpacing; s++) + { + MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme); + + if (isVertical) { - MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme); - - if (isVertical) - { - y++; - } - else - { - x++; - } + y++; + } + else + { + x++; } } } @@ -1013,11 +1136,11 @@ public class Slider : View, IOrientation if (_config._showEndSpacing) { SetAttribute ( - isSet && _config._type == SliderType.RightRange - ? Style.RangeChar.Attribute ?? normalAttr - : Style.SpaceChar.Attribute ?? normalAttr - ); - string text = isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; + isSet && _config._type == LinearRangeType.RightRange + ? Style.RangeChar.Attribute ?? normalAttr + : Style.SpaceChar.Attribute ?? normalAttr + ); + string text = isSet && _config._type == LinearRangeType.RightRange ? Style.RangeChar.Grapheme : Style.SpaceChar.Grapheme; for (var i = 0; i < remaining; i++) { @@ -1075,19 +1198,21 @@ public class Slider : View, IOrientation Move (x, y); - if (_config._sliderOrientation == Orientation.Horizontal - && _config._legendsOrientation == Orientation.Vertical) + switch (_config._linearRangeOrientation) { - x += _config._startSpacing; + case Orientation.Horizontal + when _config._legendsOrientation == Orientation.Vertical: + x += _config._startSpacing; + + break; + case Orientation.Vertical + when _config._legendsOrientation == Orientation.Horizontal: + y += _config._startSpacing; + + break; } - if (_config._sliderOrientation == Orientation.Vertical - && _config._legendsOrientation == Orientation.Horizontal) - { - y += _config._startSpacing; - } - - if (_config._sliderOrientation == Orientation.Horizontal) + if (_config._linearRangeOrientation == Orientation.Horizontal) { y += 1; } @@ -1097,43 +1222,43 @@ public class Slider : View, IOrientation x += 1; } - for (var i = 0; i < _options.Count; i++) + for (var i = 0; i < _options!.Count; i++) { var isOptionSet = false; // Check if the Option is Set. switch (_config._type) { - case SliderType.Single: - case SliderType.Multiple: + case LinearRangeType.Single: + case LinearRangeType.Multiple: if (isSet && _setOptions.Contains (i)) { isOptionSet = true; } break; - case SliderType.LeftRange: + case LinearRangeType.LeftRange: if (isSet && i <= _setOptions [0]) { isOptionSet = true; } break; - case SliderType.RightRange: + case LinearRangeType.RightRange: if (isSet && i >= _setOptions [0]) { isOptionSet = true; } break; - case SliderType.Range when _setOptions.Count == 1: + case LinearRangeType.Range when _setOptions.Count == 1: if (isSet && i == _setOptions [0]) { isOptionSet = true; } break; - case SliderType.Range: + case LinearRangeType.Range: if (isSet && ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0]))) @@ -1142,21 +1267,17 @@ public class Slider : View, IOrientation } break; + default: + throw new ArgumentOutOfRangeException (); } // Text || Abbreviation - var text = string.Empty; - if (_config._showLegendsAbbr) - { - text = _options [i].LegendAbbr.ToString () ?? new Rune (_options [i].Legend.First ()).ToString (); - } - else - { - text = _options [i].Legend; - } + string text = (_config._showLegendsAbbr ? + _options [i].LegendAbbr.ToString () : + _options [i].Legend)!; - switch (_config._sliderOrientation) + switch (_config._linearRangeOrientation) { case Orientation.Horizontal: switch (_config._legendsOrientation) @@ -1193,21 +1314,19 @@ public class Slider : View, IOrientation int legendRightSpacesCount = text.Reverse ().TakeWhile (e => e == ' ').Count (); text = text.Trim (); - // TODO(jmperricone): Improve the Orientation check. - // Calculate Start Spacing - if (_config._sliderOrientation == _config._legendsOrientation) + if (_config._linearRangeOrientation == _config._legendsOrientation) { if (i == 0) { - // The spacing for the slider use the StartSpacing but... + // The spacing for the linear range use the StartSpacing but... // The spacing for the legends is the StartSpacing MINUS the total chars to the left of the first options. // ●────●────● // Hello Bye World // // chars_left is 2 for Hello => (5 - 1) / 2 // - // then the spacing is 2 for the slider but 0 for the legends. + // then the spacing is 2 for the linear range but 0 for the legends. int charsLeft = (text.Length - 1) / 2; legendLeftSpacesCount = _config._startSpacing - charsLeft; @@ -1222,8 +1341,6 @@ public class Slider : View, IOrientation { x += legendLeftSpacesCount; } - - //Move (x, y); } // Legend @@ -1233,7 +1350,6 @@ public class Slider : View, IOrientation { MoveAndAdd (x, y, c); - //Driver.AddRune (c); if (isTextVertical) { y += 1; @@ -1245,7 +1361,7 @@ public class Slider : View, IOrientation } // Calculate End Spacing - if (i == _options.Count () - 1) + if (i == _options.Count - 1) { // See Start Spacing explanation. int charsRight = text.Length / 2; @@ -1264,15 +1380,18 @@ public class Slider : View, IOrientation x += legendRightSpacesCount; } - if (_config._sliderOrientation == Orientation.Horizontal - && _config._legendsOrientation == Orientation.Vertical) + switch (_config._linearRangeOrientation) { - x += _config._cachedInnerSpacing + 1; - } - else if (_config._sliderOrientation == Orientation.Vertical - && _config._legendsOrientation == Orientation.Horizontal) - { - y += _config._cachedInnerSpacing + 1; + case Orientation.Horizontal + when _config._legendsOrientation == Orientation.Vertical: + x += _config._cachedInnerSpacing + 1; + + break; + case Orientation.Vertical + when _config._legendsOrientation == Orientation.Horizontal: + y += _config._cachedInnerSpacing + 1; + + break; } } } @@ -1290,12 +1409,6 @@ public class Slider : View, IOrientation /// protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { - // Note(jmperricone): Maybe we click to focus the cursor, and on next click we set the option. - // That will make OptionFocused Event more relevant. - // (tig: I don't think so. Maybe an option if someone really wants it, but for now that - // adds too much friction to UI. - // TODO(jmperricone): Make Range Type work with mouse. - if (!(mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) || mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition) @@ -1320,6 +1433,9 @@ public class Slider : View, IOrientation return true; } + bool success; + int option; + if (_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition) && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) @@ -1328,9 +1444,6 @@ public class Slider : View, IOrientation _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); - var success = false; - var option = 0; - // how far has user dragged from original location? if (Orientation == Orientation.Horizontal) { @@ -1354,51 +1467,49 @@ public class Slider : View, IOrientation return true; } - if ((_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Released)) - || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) + if ((!_dragPosition.HasValue || !mouseEvent.Flags.HasFlag (MouseFlags.Button1Released)) + && !mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { - // End Drag - App?.Mouse.UngrabMouse (); - _dragPosition = null; - _moveRenderPosition = null; - - // TODO: Add func to calc distance between options to use as the MouseClickXOptionThreshold - var success = false; - var option = 0; - - if (Orientation == Orientation.Horizontal) - { - success = TryGetOptionByPosition (mouseEvent.Position.X, 0, Math.Max (0, _config._cachedInnerSpacing / 2), out option); - } - else - { - success = TryGetOptionByPosition (0, mouseEvent.Position.Y, Math.Max (0, _config._cachedInnerSpacing / 2), out option); - } - - if (success) - { - if (!OnOptionFocused (option, new (GetSetOptionDictionary (), FocusedOption))) - { - SetFocusedOption (); - } - } - - SetNeedsDraw (); - - mouseEvent.Handled = true; - + return mouseEvent.Handled; } + // End Drag + App?.Mouse.UngrabMouse (); + _dragPosition = null; + _moveRenderPosition = null; + + switch (Orientation) + { + case Orientation.Horizontal: + success = TryGetOptionByPosition (mouseEvent.Position.X, 0, Math.Max (0, _config._cachedInnerSpacing / 2), out option); + + break; + default: + success = TryGetOptionByPosition (0, mouseEvent.Position.Y, Math.Max (0, _config._cachedInnerSpacing / 2), out option); + + break; + } + + if (success) + { + if (!OnOptionFocused (option, new (GetSetOptionDictionary (), FocusedOption))) + { + SetFocusedOption (); + } + } + + SetNeedsDraw (); + + mouseEvent.Handled = true; + return mouseEvent.Handled; Point ClampMovePosition (Point position) { - int Clamp (int value, int min, int max) { return Math.Max (min, Math.Min (max, value)); } - if (Orientation == Orientation.Horizontal) { int left = _config._startSpacing; - int width = _options.Count + (_options.Count - 1) * _config._cachedInnerSpacing; + int width = _options!.Count + (_options.Count - 1) * _config._cachedInnerSpacing; int right = left + width - 1; int clampedX = Clamp (position.X, left, right); position = new (clampedX, 0); @@ -1406,13 +1517,15 @@ public class Slider : View, IOrientation else { int top = _config._startSpacing; - int height = _options.Count + (_options.Count - 1) * _config._cachedInnerSpacing; + int height = _options!.Count + (_options.Count - 1) * _config._cachedInnerSpacing; int bottom = top + height - 1; int clampedY = Clamp (position.Y, top, bottom); position = new (0, clampedY); } return position; + + static int Clamp (int value, int min, int max) { return Math.Max (min, Math.Min (max, value)); } } } @@ -1427,7 +1540,7 @@ public class Slider : View, IOrientation AddCommand (Command.RightExtend, () => ExtendPlus ()); AddCommand (Command.LeftExtend, () => ExtendMinus ()); AddCommand (Command.Activate, () => Select ()); - AddCommand (Command.Accept, (ctx) => Accept (ctx)); + AddCommand (Command.Accept, ctx => Accept (ctx)); SetKeyBindings (); } @@ -1435,7 +1548,7 @@ public class Slider : View, IOrientation // This is called during initialization and anytime orientation changes private void SetKeyBindings () { - if (_config._sliderOrientation == Orientation.Horizontal) + if (_config._linearRangeOrientation == Orientation.Horizontal) { KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Remove (Key.CursorDown); @@ -1470,10 +1583,13 @@ public class Slider : View, IOrientation KeyBindings.Add (Key.Space, Command.Activate); } - private Dictionary> GetSetOptionDictionary () { return _setOptions.ToDictionary (e => e, e => _options [e]); } + private Dictionary> GetSetOptionDictionary () + { + return _setOptions.ToDictionary (e => e, e => _options! [e]); + } /// - /// Sets or unsets based on . + /// Sets or unsets based on . /// /// The option to change. /// If , sets the option. Unsets it otherwise. @@ -1484,7 +1600,8 @@ public class Slider : View, IOrientation if (!_setOptions.Contains (optionIndex)) { _setOptions.Add (optionIndex); - _options [optionIndex].OnSet (); + + _options? [optionIndex].OnSet (); } } else @@ -1492,7 +1609,8 @@ public class Slider : View, IOrientation if (_setOptions.Contains (optionIndex)) { _setOptions.Remove (optionIndex); - _options [optionIndex].OnUnSet (); + + _options? [optionIndex].OnUnSet (); } } @@ -1502,16 +1620,18 @@ public class Slider : View, IOrientation private bool SetFocusedOption () { - if (_options.Count == 0) + if (_options is null or { Count: 0 }) { return false; } - bool changed = false; + + var changed = false; + switch (_config._type) { - case SliderType.Single: - case SliderType.LeftRange: - case SliderType.RightRange: + case LinearRangeType.Single: + case LinearRangeType.LeftRange: + case LinearRangeType.RightRange: if (_setOptions.Count == 1) { @@ -1542,7 +1662,7 @@ public class Slider : View, IOrientation changed = true; break; - case SliderType.Multiple: + case LinearRangeType.Multiple: if (_setOptions.Contains (FocusedOption)) { if (!_config._allowEmpty && _setOptions.Count () == 1) @@ -1564,7 +1684,7 @@ public class Slider : View, IOrientation break; - case SliderType.Range: + case LinearRangeType.Range: if (_config._rangeAllowSingle) { if (_setOptions.Count == 1) @@ -1692,7 +1812,7 @@ public class Slider : View, IOrientation } } - // Raise Slider Option Changed Event. + // Raise LinearRange Option Changed Event. OnOptionsChanged (); changed = true; @@ -1706,7 +1826,7 @@ public class Slider : View, IOrientation internal bool ExtendPlus () { - int next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; + int next = _options is { } && FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; if (next != FocusedOption && !OnOptionFocused ( @@ -1721,48 +1841,6 @@ public class Slider : View, IOrientation } return true; - - //// TODO: Support RangeMultiple - //if (_setOptions.Contains (FocusedOption)) { - // var next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; - // if (!_setOptions.Contains (next)) { - // if (_config._type == SliderType.Range) { - // if (_setOptions.Count == 1) { - // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { - // _setOptions.Add (FocusedOption); - // _setOptions.Sort (); // Range Type - // OnOptionsChanged (); - // } - // } else if (_setOptions.Count == 2) { - // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { - // _setOptions [1] = FocusedOption; - // _setOptions.Sort (); // Range Type - // OnOptionsChanged (); - // } - // } - // } else { - // _setOptions.Remove (FocusedOption); - // // Note(jmperricone): We are setting the option here, do we send the OptionFocused Event too ? - - // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { - // _setOptions.Add (FocusedOption); - // _setOptions.Sort (); // Range Type - // OnOptionsChanged (); - // } - // } - // } else { - // if (_config._type == SliderType.Range) { - // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { - // _setOptions.Clear(); - // _setOptions.Add (FocusedOption); - // OnOptionsChanged (); - // } - // } else if (/*_settingRange == true ||*/ !AllowEmpty) { - // SetFocusedOption (); - // } - // } - //} - //return true; } internal bool ExtendMinus () @@ -1784,12 +1862,9 @@ public class Slider : View, IOrientation return true; } - internal bool Select () - { - return SetFocusedOption (); - } + internal bool Select () => SetFocusedOption (); - internal bool Accept (ICommandContext commandContext) + internal bool Accept (ICommandContext? commandContext) { SetFocusedOption (); @@ -1853,7 +1928,7 @@ public class Slider : View, IOrientation internal bool MoveEnd () { - if (OnOptionFocused (_options.Count - 1, new (GetSetOptionDictionary (), FocusedOption))) + if (OnOptionFocused (_options!.Count - 1, new (GetSetOptionDictionary (), FocusedOption))) { return false; } diff --git a/Terminal.Gui/Views/Slider/SliderAttributes.cs b/Terminal.Gui/Views/LinearRange/LinearRangeAttributes.cs similarity index 80% rename from Terminal.Gui/Views/Slider/SliderAttributes.cs rename to Terminal.Gui/Views/LinearRange/LinearRangeAttributes.cs index c57c8ab5a..e39261241 100644 --- a/Terminal.Gui/Views/Slider/SliderAttributes.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeAttributes.cs @@ -1,7 +1,7 @@ namespace Terminal.Gui.Views; -/// Legend Style -public class SliderAttributes +/// Legend Style +public class LinearRangeAttributes { /// Attribute for the Legends Container. public Attribute? EmptyAttribute { get; set; } diff --git a/Terminal.Gui/Views/Slider/SliderConfiguration.cs b/Terminal.Gui/Views/LinearRange/LinearRangeConfiguration.cs similarity index 63% rename from Terminal.Gui/Views/Slider/SliderConfiguration.cs rename to Terminal.Gui/Views/LinearRange/LinearRangeConfiguration.cs index 1b9354a48..f0a94b62d 100644 --- a/Terminal.Gui/Views/Slider/SliderConfiguration.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeConfiguration.cs @@ -1,8 +1,7 @@ - namespace Terminal.Gui.Views; -/// All configuration are grouped in this class. -internal class SliderConfiguration +/// All configuration are grouped in this class. +internal class LinearRangeConfiguration { internal bool _allowEmpty; internal int _endSpacing; @@ -13,8 +12,8 @@ internal class SliderConfiguration internal bool _showEndSpacing; internal bool _showLegends; internal bool _showLegendsAbbr; - internal Orientation _sliderOrientation = Orientation.Horizontal; + internal Orientation _linearRangeOrientation = Orientation.Horizontal; internal int _startSpacing; - internal SliderType _type = SliderType.Single; + internal LinearRangeType _type = LinearRangeType.Single; internal bool _useMinimumSize; } diff --git a/Terminal.Gui/Views/Slider/SliderEventArgs.cs b/Terminal.Gui/Views/LinearRange/LinearRangeEventArgs.cs similarity index 60% rename from Terminal.Gui/Views/Slider/SliderEventArgs.cs rename to Terminal.Gui/Views/LinearRange/LinearRangeEventArgs.cs index 0b55b4012..36442c8cb 100644 --- a/Terminal.Gui/Views/Slider/SliderEventArgs.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeEventArgs.cs @@ -1,13 +1,12 @@ -#nullable disable namespace Terminal.Gui.Views; -/// for events. -public class SliderEventArgs : EventArgs +/// for events. +public class LinearRangeEventArgs : EventArgs { - /// Initializes a new instance of + /// Initializes a new instance of /// The current options. /// Index of the option that is focused. -1 if no option has the focus. - public SliderEventArgs (Dictionary> options, int focused = -1) + public LinearRangeEventArgs (Dictionary> options, int focused = -1) { Options = options; Focused = focused; @@ -21,5 +20,5 @@ public class SliderEventArgs : EventArgs public int Focused { get; set; } /// Gets/sets whether the option is set or not. - public Dictionary> Options { get; set; } + public Dictionary> Options { get; set; } } diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs b/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs new file mode 100644 index 000000000..47fc52580 --- /dev/null +++ b/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs @@ -0,0 +1,50 @@ +namespace Terminal.Gui.Views; + + /// Represents an option in a . + /// Data type of the option. + public class LinearRangeOption +{ + /// Creates a new empty instance of the class. + public LinearRangeOption () { } + + /// Creates a new instance of the class with values for each property. + public LinearRangeOption (string legend, Rune legendAbbr, T data) + { + Legend = legend; + LegendAbbr = legendAbbr; + Data = data; + } + + /// Event fired when an option has changed. + public event EventHandler? Changed; + + /// Custom data of the option. + public T? Data { get; set; } + + /// Legend of the option. + public string? Legend { get; set; } + + /// + /// Abbreviation of the Legend. When the too small to fit + /// . + /// + public Rune LegendAbbr { get; set; } + + /// Event Raised when this option is set. + public event EventHandler? Set; + + /// Creates a human-readable string that represents this . + public override string ToString () => "{Legend=" + Legend + ", LegendAbbr=" + LegendAbbr + ", Data=" + Data + "}"; + + /// Event Raised when this option is unset. + public event EventHandler? UnSet; + + /// To Raise the event from the LinearRange. + internal void OnChanged (bool isSet) { Changed?.Invoke (this, new (isSet)); } + + /// To Raise the event from the LinearRange. + internal void OnSet () { Set?.Invoke (this, new (true)); } + + /// To Raise the event from the LinearRange. + internal void OnUnSet () { UnSet?.Invoke (this, new (false)); } +} diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeOptionEventArgs.cs b/Terminal.Gui/Views/LinearRange/LinearRangeOptionEventArgs.cs new file mode 100644 index 000000000..77a408878 --- /dev/null +++ b/Terminal.Gui/Views/LinearRange/LinearRangeOptionEventArgs.cs @@ -0,0 +1,12 @@ +namespace Terminal.Gui.Views; + +/// for events. +public class LinearRangeOptionEventArgs : EventArgs +{ + /// Initializes a new instance of + /// indicates whether the option is set + public LinearRangeOptionEventArgs (bool isSet) { IsSet = isSet; } + + /// Gets whether the option is set or not. + public bool IsSet { get; } +} diff --git a/Terminal.Gui/Views/Slider/SliderStyle.cs b/Terminal.Gui/Views/LinearRange/LinearRangeStyle.cs similarity index 64% rename from Terminal.Gui/Views/Slider/SliderStyle.cs rename to Terminal.Gui/Views/LinearRange/LinearRangeStyle.cs index aa5557116..aef55dbe7 100644 --- a/Terminal.Gui/Views/Slider/SliderStyle.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeStyle.cs @@ -1,36 +1,35 @@ - -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; -/// Style -public class SliderStyle +/// Style +public class LinearRangeStyle { /// Constructs a new instance. - public SliderStyle () { LegendAttributes = new (); } + public LinearRangeStyle () { LegendAttributes = new (); } /// The glyph and the attribute to indicate mouse dragging. public Cell DragChar { get; set; } - /// The glyph and the attribute used for empty spaces on the slider. + /// The glyph and the attribute used for empty spaces on the linear range. public Cell EmptyChar { get; set; } - /// The glyph and the attribute used for the end of ranges on the slider. + /// The glyph and the attribute used for the end of ranges on the linear range. public Cell EndRangeChar { get; set; } /// Legend attributes - public SliderAttributes LegendAttributes { get; set; } + public LinearRangeAttributes LegendAttributes { get; set; } - /// The glyph and the attribute used for each option (tick) on the slider. + /// The glyph and the attribute used for each option (tick) on the linear range. public Cell OptionChar { get; set; } - /// The glyph and the attribute used for filling in ranges on the slider. + /// The glyph and the attribute used for filling in ranges on the linear range. public Cell RangeChar { get; set; } - /// The glyph and the attribute used for options (ticks) that are set on the slider. + /// The glyph and the attribute used for options (ticks) that are set on the linear range. public Cell SetChar { get; set; } - /// The glyph and the attribute used for spaces between options (ticks) on the slider. + /// The glyph and the attribute used for spaces between options (ticks) on the linear range. public Cell SpaceChar { get; set; } - /// The glyph and the attribute used for the start of ranges on the slider. + /// The glyph and the attribute used for the start of ranges on the linear range. public Cell StartRangeChar { get; set; } } diff --git a/Terminal.Gui/Views/Slider/SliderType.cs b/Terminal.Gui/Views/LinearRange/LinearRangeType.cs similarity index 91% rename from Terminal.Gui/Views/Slider/SliderType.cs rename to Terminal.Gui/Views/LinearRange/LinearRangeType.cs index 7715aa3c3..0633a416e 100644 --- a/Terminal.Gui/Views/Slider/SliderType.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeType.cs @@ -1,7 +1,7 @@ namespace Terminal.Gui.Views; -/// Types -public enum SliderType +/// Types +public enum LinearRangeType { /// /// diff --git a/Terminal.Gui/Views/Slider/SliderOption.cs b/Terminal.Gui/Views/Slider/SliderOption.cs deleted file mode 100644 index d5207c301..000000000 --- a/Terminal.Gui/Views/Slider/SliderOption.cs +++ /dev/null @@ -1,51 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; - -/// Represents an option in a . -/// Data type of the option. -public class SliderOption -{ - /// Creates a new empty instance of the class. - public SliderOption () { } - - /// Creates a new instance of the class with values for each property. - public SliderOption (string legend, Rune legendAbbr, T data) - { - Legend = legend; - LegendAbbr = legendAbbr; - Data = data; - } - - /// Event fired when an option has changed. - public event EventHandler Changed; - - /// Custom data of the option. - public T Data { get; set; } - - /// Legend of the option. - public string Legend { get; set; } - - /// - /// Abbreviation of the Legend. When the too small to fit - /// . - /// - public Rune LegendAbbr { get; set; } - - /// Event Raised when this option is set. - public event EventHandler Set; - - /// Creates a human-readable string that represents this . - public override string ToString () { return "{Legend=" + Legend + ", LegendAbbr=" + LegendAbbr + ", Data=" + Data + "}"; } - - /// Event Raised when this option is unset. - public event EventHandler UnSet; - - /// To Raise the event from the Slider. - internal void OnChanged (bool isSet) { Changed?.Invoke (this, new (isSet)); } - - /// To Raise the event from the Slider. - internal void OnSet () { Set?.Invoke (this, new (true)); } - - /// To Raise the event from the Slider. - internal void OnUnSet () { UnSet?.Invoke (this, new (false)); } -} diff --git a/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs b/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs deleted file mode 100644 index 5176f9601..000000000 --- a/Terminal.Gui/Views/Slider/SliderOptionEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable disable -namespace Terminal.Gui.Views; - -/// for events. -public class SliderOptionEventArgs : EventArgs -{ - /// Initializes a new instance of - /// indicates whether the option is set - public SliderOptionEventArgs (bool isSet) { IsSet = isSet; } - - /// Gets whether the option is set or not. - public bool IsSet { get; } -} diff --git a/Tests/IntegrationTests/FluentTests/LinearRangeFluentTests.cs b/Tests/IntegrationTests/FluentTests/LinearRangeFluentTests.cs new file mode 100644 index 000000000..6d526c7e7 --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/LinearRangeFluentTests.cs @@ -0,0 +1,144 @@ +using TerminalGuiFluentTesting; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +public class LinearRangeFluentTests (ITestOutputHelper outputHelper) +{ + private readonly TextWriter _out = new TestOutputWriter (outputHelper); + + [Theory] + [ClassData (typeof (TestDrivers))] + public void LinearRange_CanCreateAndRender (TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d, _out) + .Add ( + new LinearRange (new() { 0, 10, 20, 30, 40, 50 }) + { + X = 2, + Y = 2, + Type = LinearRangeType.Single + }) + .Focus> () + .WaitIteration () + .ScreenShot ("LinearRange initial render", _out) + .Stop (); + } + + [Theory] + [ClassData (typeof (TestDrivers))] + public void LinearRange_CanNavigateWithArrowKeys (TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d, _out) + .Add ( + new LinearRange (new() { 0, 10, 20, 30 }) + { + X = 2, + Y = 2, + Type = LinearRangeType.Single, + AllowEmpty = false + }) + .Focus> () + .WaitIteration () + .ScreenShot ("Initial state", _out) + .EnqueueKeyEvent (Key.CursorRight) + .WaitIteration () + .ScreenShot ("After right arrow", _out) + .EnqueueKeyEvent (Key.CursorRight) + .WaitIteration () + .ScreenShot ("After second right arrow", _out) + .Stop (); + } + + [Theory] + [ClassData (typeof (TestDrivers))] + public void LinearRange_TypeChange_TriggersEvents (TestDriver d) + { + LinearRange linearRange = new (new() { 0, 10, 20, 30 }) + { + X = 2, + Y = 2, + Type = LinearRangeType.Single + }; + + var changingEventRaised = false; + var changedEventRaised = false; + + linearRange.TypeChanging += (_, args) => + { + changingEventRaised = true; + Assert.Equal (LinearRangeType.Single, args.CurrentValue); + Assert.Equal (LinearRangeType.Range, args.NewValue); + }; + + linearRange.TypeChanged += (_, args) => + { + changedEventRaised = true; + Assert.Equal (LinearRangeType.Single, args.OldValue); + Assert.Equal (LinearRangeType.Range, args.NewValue); + }; + + // Change the type before adding to window + linearRange.Type = LinearRangeType.Range; + + using GuiTestContext c = With.A (80, 25, d, _out) + .Add (linearRange) + .Focus> () + .WaitIteration () + .ScreenShot ("After type change to Range", _out) + .Stop (); + + Assert.True (changingEventRaised); + Assert.True (changedEventRaised); + Assert.Equal (LinearRangeType.Range, linearRange.Type); + } + + [Theory] + [ClassData (typeof (TestDrivers))] + public void LinearRange_RangeType_CanSelectRange (TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d, _out) + .Add ( + new LinearRange (new() { 0, 10, 20, 30, 40 }) + { + X = 2, + Y = 2, + Type = LinearRangeType.Range, + AllowEmpty = false + }) + .Focus> () + .WaitIteration () + .ScreenShot ("Range type initial", _out) + .EnqueueKeyEvent (Key.Space) + .WaitIteration () + .ScreenShot ("After first selection", _out) + .EnqueueKeyEvent (Key.CursorRight.WithCtrl) + .WaitIteration () + .EnqueueKeyEvent (Key.CursorRight.WithCtrl) + .WaitIteration () + .ScreenShot ("After extending range", _out) + .Stop (); + } + + [Theory] + [ClassData (typeof (TestDrivers))] + public void LinearRange_VerticalOrientation_Renders (TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d, _out) + .Add ( + new LinearRange (new() { 0, 10, 20, 30 }) + { + X = 2, + Y = 2, + Orientation = Orientation.Vertical, + Type = LinearRangeType.Single + }) + .Focus> () + .WaitIteration () + .ScreenShot ("Vertical orientation", _out) + .EnqueueKeyEvent (Key.CursorDown) + .WaitIteration () + .ScreenShot ("After down arrow", _out) + .Stop (); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/PaddingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/PaddingTests.cs index c81194394..7e211d16e 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/PaddingTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/PaddingTests.cs @@ -1,10 +1,6 @@ -#nullable enable -using UnitTests; -using Xunit.Abstractions; +namespace ViewBaseTests.Adornments; -namespace ViewBaseTests.Adornments; - -public class PaddingTests (ITestOutputHelper output) +public class PaddingTests { [Fact] public void Constructor_Defaults () @@ -21,5 +17,4 @@ public class PaddingTests (ITestOutputHelper output) View view = new () { Height = 3, Width = 3 }; Assert.Equal (Thickness.Empty, view.Padding!.Thickness); } - } diff --git a/Tests/UnitTestsParallelizable/Views/SliderTests.cs b/Tests/UnitTestsParallelizable/Views/LinearRangeTests.cs similarity index 50% rename from Tests/UnitTestsParallelizable/Views/SliderTests.cs rename to Tests/UnitTestsParallelizable/Views/LinearRangeTests.cs index 9aa71097d..b39432f05 100644 --- a/Tests/UnitTestsParallelizable/Views/SliderTests.cs +++ b/Tests/UnitTestsParallelizable/Views/LinearRangeTests.cs @@ -3,13 +3,13 @@ using UnitTests; namespace ViewsTests; -public class SliderOptionTests : FakeDriverBase +public class LinearRangeOptionTests : FakeDriverBase { [Fact] public void OnChanged_Should_Raise_ChangedEvent () { // Arrange - SliderOption sliderOption = new (); + LinearRangeOption sliderOption = new (); var eventRaised = false; sliderOption.Changed += (sender, args) => eventRaised = true; @@ -24,7 +24,7 @@ public class SliderOptionTests : FakeDriverBase public void OnSet_Should_Raise_SetEvent () { // Arrange - SliderOption sliderOption = new (); + LinearRangeOption sliderOption = new (); var eventRaised = false; sliderOption.Set += (sender, args) => eventRaised = true; @@ -39,7 +39,7 @@ public class SliderOptionTests : FakeDriverBase public void OnUnSet_Should_Raise_UnSetEvent () { // Arrange - SliderOption sliderOption = new (); + LinearRangeOption sliderOption = new (); var eventRaised = false; sliderOption.UnSet += (sender, args) => eventRaised = true; @@ -51,42 +51,42 @@ public class SliderOptionTests : FakeDriverBase } [Fact] - public void Slider_Option_Default_Constructor () + public void LinearRange_Option_Default_Constructor () { - SliderOption o = new (); + LinearRangeOption o = new (); Assert.Null (o.Legend); Assert.Equal (default (Rune), o.LegendAbbr); Assert.Equal (default (int), o.Data); } [Fact] - public void Slider_Option_Values_Constructor () + public void LinearRange_Option_Values_Constructor () { - SliderOption o = new ("1 thousand", new ('y'), 1000); + LinearRangeOption o = new ("1 thousand", new ('y'), 1000); Assert.Equal ("1 thousand", o.Legend); Assert.Equal (new ('y'), o.LegendAbbr); Assert.Equal (1000, o.Data); } [Fact] - public void SliderOption_ToString_WhenEmpty () + public void LinearRangeOption_ToString_WhenEmpty () { - SliderOption sliderOption = new (); + LinearRangeOption sliderOption = new (); Assert.Equal ("{Legend=, LegendAbbr=\0, Data=}", sliderOption.ToString ()); } [Fact] - public void SliderOption_ToString_WhenPopulated_WithInt () + public void LinearRangeOption_ToString_WhenPopulated_WithInt () { - SliderOption sliderOption = new () { Legend = "Lord flibble", LegendAbbr = new ('l'), Data = 1 }; + LinearRangeOption sliderOption = new () { Legend = "Lord flibble", LegendAbbr = new ('l'), Data = 1 }; Assert.Equal ("{Legend=Lord flibble, LegendAbbr=l, Data=1}", sliderOption.ToString ()); } [Fact] - public void SliderOption_ToString_WhenPopulated_WithSizeF () + public void LinearRangeOption_ToString_WhenPopulated_WithSizeF () { - SliderOption sliderOption = new () + LinearRangeOption sliderOption = new () { Legend = "Lord flibble", LegendAbbr = new ('l'), Data = new (32, 11) }; @@ -95,17 +95,17 @@ public class SliderOptionTests : FakeDriverBase } } -public class SliderEventArgsTests : FakeDriverBase +public class LinearRangeEventArgsTests : FakeDriverBase { [Fact] public void Constructor_Sets_Cancel_Default_To_False () { // Arrange - Dictionary> options = new (); + Dictionary> options = new (); var focused = 42; // Act - SliderEventArgs sliderEventArgs = new (options, focused); + LinearRangeEventArgs sliderEventArgs = new (options, focused); // Assert Assert.False (sliderEventArgs.Cancel); @@ -115,11 +115,11 @@ public class SliderEventArgsTests : FakeDriverBase public void Constructor_Sets_Focused () { // Arrange - Dictionary> options = new (); + Dictionary> options = new (); var focused = 42; // Act - SliderEventArgs sliderEventArgs = new (options, focused); + LinearRangeEventArgs sliderEventArgs = new (options, focused); // Assert Assert.Equal (focused, sliderEventArgs.Focused); @@ -129,23 +129,23 @@ public class SliderEventArgsTests : FakeDriverBase public void Constructor_Sets_Options () { // Arrange - Dictionary> options = new (); + Dictionary> options = new (); // Act - SliderEventArgs sliderEventArgs = new (options); + LinearRangeEventArgs sliderEventArgs = new (options); // Assert Assert.Equal (options, sliderEventArgs.Options); } } -public class SliderTests : FakeDriverBase +public class LinearRangeTests : FakeDriverBase { [Fact] public void Constructor_Default () { // Arrange & Act - Slider slider = new (); + LinearRange slider = new (); // Assert Assert.NotNull (slider); @@ -155,7 +155,7 @@ public class SliderTests : FakeDriverBase Assert.False (slider.AllowEmpty); Assert.True (slider.ShowLegends); Assert.False (slider.ShowEndSpacing); - Assert.Equal (SliderType.Single, slider.Type); + Assert.Equal (LinearRangeType.Single, slider.Type); Assert.Equal (1, slider.MinimumInnerSpacing); Assert.True (slider.Width is DimAuto); Assert.True (slider.Height is DimAuto); @@ -169,15 +169,15 @@ public class SliderTests : FakeDriverBase List options = new () { 1, 2, 3 }; // Act - Slider slider = new (options); + LinearRange slider = new (options); slider.SetRelativeLayout (new (100, 100)); // Assert // 0123456789 // 1 2 3 Assert.Equal (1, slider.MinimumInnerSpacing); - Assert.Equal (new Size (5, 2), slider.GetContentSize ()); - Assert.Equal (new Size (5, 2), slider.Frame.Size); + Assert.Equal (new (5, 2), slider.GetContentSize ()); + Assert.Equal (new (5, 2), slider.Frame.Size); Assert.NotNull (slider); Assert.NotNull (slider.Options); Assert.Equal (options.Count, slider.Options.Count); @@ -187,7 +187,7 @@ public class SliderTests : FakeDriverBase public void MovePlus_Should_MoveFocusRight_When_OptionIsAvailable () { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); // Act bool result = slider.MovePlus (); @@ -201,7 +201,7 @@ public class SliderTests : FakeDriverBase public void MovePlus_Should_NotMoveFocusRight_When_AtEnd () { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); slider.FocusedOption = 3; @@ -217,7 +217,7 @@ public class SliderTests : FakeDriverBase public void OnOptionFocused_Event_Cancelled () { // Arrange - Slider slider = new (new () { 1, 2, 3 }); + LinearRange slider = new (new () { 1, 2, 3 }); var eventRaised = false; var cancel = false; slider.OptionFocused += (sender, args) => eventRaised = true; @@ -226,7 +226,7 @@ public class SliderTests : FakeDriverBase // Create args with cancel set to false cancel = false; - SliderEventArgs args = + LinearRangeEventArgs args = new (new (), newFocusedOption) { Cancel = cancel }; Assert.Equal (0, slider.FocusedOption); @@ -257,11 +257,11 @@ public class SliderTests : FakeDriverBase public void OnOptionFocused_Event_Raised () { // Arrange - Slider slider = new (new () { 1, 2, 3 }); + LinearRange slider = new (new () { 1, 2, 3 }); var eventRaised = false; slider.OptionFocused += (sender, args) => eventRaised = true; var newFocusedOption = 1; - SliderEventArgs args = new (new (), newFocusedOption); + LinearRangeEventArgs args = new (new (), newFocusedOption); // Act slider.OnOptionFocused (newFocusedOption, args); @@ -274,7 +274,7 @@ public class SliderTests : FakeDriverBase public void OnOptionsChanged_Event_Raised () { // Arrange - Slider slider = new (); + LinearRange slider = new (); var eventRaised = false; slider.OptionsChanged += (sender, args) => eventRaised = true; @@ -289,7 +289,7 @@ public class SliderTests : FakeDriverBase public void Set_Should_Not_UnSetFocusedOption_When_EmptyNotAllowed () { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }) { AllowEmpty = false }; + LinearRange slider = new (new () { 1, 2, 3, 4 }) { AllowEmpty = false }; Assert.NotEmpty (slider.GetSetOptions ()); @@ -307,7 +307,7 @@ public class SliderTests : FakeDriverBase public void Set_Should_SetFocusedOption () { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); // Act slider.FocusedOption = 2; @@ -323,7 +323,7 @@ public class SliderTests : FakeDriverBase public void TryGetOptionByPosition_InvalidPosition_Failure () { // Arrange - Slider slider = new (new () { 1, 2, 3 }); + LinearRange slider = new (new () { 1, 2, 3 }); var x = 10; var y = 10; var threshold = 2; @@ -347,7 +347,8 @@ public class SliderTests : FakeDriverBase public void TryGetOptionByPosition_ValidPositionHorizontal_Success (int x, int y, int threshold, int expectedData) { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); + // 0123456789 // 1234 @@ -376,7 +377,7 @@ public class SliderTests : FakeDriverBase public void TryGetOptionByPosition_ValidPositionVertical_Success (int x, int y, int threshold, int expectedData) { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); slider.Orientation = Orientation.Vertical; // Set auto size to true to enable testing @@ -405,7 +406,7 @@ public class SliderTests : FakeDriverBase public void TryGetPositionByOption_InvalidOption_Failure () { // Arrange - Slider slider = new (new () { 1, 2, 3 }); + LinearRange slider = new (new () { 1, 2, 3 }); int option = -1; (int, int) expectedPosition = (-1, -1); @@ -424,7 +425,7 @@ public class SliderTests : FakeDriverBase public void TryGetPositionByOption_ValidOptionHorizontal_Success (int option, int expectedX, int expectedY) { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); // Set auto size to true to enable testing slider.MinimumInnerSpacing = 2; @@ -448,7 +449,7 @@ public class SliderTests : FakeDriverBase public void TryGetPositionByOption_ValidOptionVertical_Success (int option, int expectedX, int expectedY) { // Arrange - Slider slider = new (new () { 1, 2, 3, 4 }); + LinearRange slider = new (new () { 1, 2, 3, 4 }); slider.Orientation = Orientation.Vertical; // Set auto size to true to enable testing @@ -468,32 +469,12 @@ public class SliderTests : FakeDriverBase private void One_Option_Does_Not_Throw () { // Arrange - Slider slider = new (); + LinearRange slider = new (); slider.BeginInit (); slider.EndInit (); // Act/Assert - slider.Options = new () { new () }; - } - - [Fact] - private void Set_Options_No_Legend_Throws () - { - // Arrange - Slider slider = new (); - - // Act/Assert - Assert.Throws (() => slider.Options = null); - } - - [Fact] - private void Set_Options_Throws_If_Null () - { - // Arrange - Slider slider = new (); - - // Act/Assert - Assert.Throws (() => slider.Options = null); + slider.Options = [new ()]; } [Fact] @@ -505,12 +486,12 @@ public class SliderTests : FakeDriverBase Height = Dim.Fill () }; - List options = new () { "01234", "01234" }; + List options = ["01234", "01234"]; - Slider slider = new (options) + LinearRange slider = new (options) { Orientation = Orientation.Vertical, - Type = SliderType.Multiple, + Type = LinearRangeType.Multiple }; view.Add (slider); view.BeginInit (); @@ -539,10 +520,10 @@ public class SliderTests : FakeDriverBase List options = new () { "01234", "01234" }; - Slider slider = new (options) + LinearRange slider = new (options) { Orientation = Orientation.Vertical, - Type = SliderType.Multiple, + Type = LinearRangeType.Multiple, Height = 10 }; view.Add (slider); @@ -572,11 +553,11 @@ public class SliderTests : FakeDriverBase List options = new () { "01234", "01234" }; - Slider slider = new (options) + LinearRange slider = new (options) { Orientation = Orientation.Vertical, - Type = SliderType.Multiple, - Width = 10, + Type = LinearRangeType.Multiple, + Width = 10 }; view.Add (slider); view.BeginInit (); @@ -596,3 +577,254 @@ public class SliderTests : FakeDriverBase // Add more tests for different scenarios and edge cases. } +public class LinearRangeCWPTests : FakeDriverBase +{ + [Fact] + public void Type_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = LinearRangeType.Single; + var newValue = LinearRangeType.Range; + + linearRange.TypeChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.TypeChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.Type = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.Type); + } + + [Fact] + public void Type_PropertyChange_CanBeCancelled () + { + // Arrange + LinearRange linearRange = new (); + LinearRangeType oldValue = linearRange.Type; + + linearRange.TypeChanging += (sender, args) => { args.Handled = true; }; + + // Act + linearRange.Type = LinearRangeType.Range; + + // Assert + Assert.Equal (oldValue, linearRange.Type); + } + + [Fact] + public void LegendsOrientation_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = Orientation.Horizontal; + var newValue = Orientation.Vertical; + + linearRange.LegendsOrientationChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.LegendsOrientationChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.LegendsOrientation = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.LegendsOrientation); + } + + [Fact] + public void MinimumInnerSpacing_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = 1; + var newValue = 5; + + linearRange.MinimumInnerSpacingChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.MinimumInnerSpacingChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.MinimumInnerSpacing = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.MinimumInnerSpacing); + } + + [Fact] + public void ShowLegends_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = true; + var newValue = false; + + linearRange.ShowLegendsChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.ShowLegendsChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.ShowLegends = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.ShowLegends); + } + + [Fact] + public void ShowEndSpacing_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = false; + var newValue = true; + + linearRange.ShowEndSpacingChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.ShowEndSpacingChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.ShowEndSpacing = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.ShowEndSpacing); + } + + [Fact] + public void UseMinimumSize_PropertyChange_RaisesChangingAndChangedEvents () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + var oldValue = false; + var newValue = true; + + linearRange.UseMinimumSizeChanging += (sender, args) => + { + changingRaised = true; + Assert.Equal (oldValue, args.CurrentValue); + Assert.Equal (newValue, args.NewValue); + }; + + linearRange.UseMinimumSizeChanged += (sender, args) => + { + changedRaised = true; + Assert.Equal (oldValue, args.OldValue); + Assert.Equal (newValue, args.NewValue); + }; + + // Act + linearRange.UseMinimumSize = newValue; + + // Assert + Assert.True (changingRaised); + Assert.True (changedRaised); + Assert.Equal (newValue, linearRange.UseMinimumSize); + } + + [Fact] + public void Type_PropertyChange_NoEventsWhenValueUnchanged () + { + // Arrange + LinearRange linearRange = new (); + var changingRaised = false; + var changedRaised = false; + + linearRange.TypeChanging += (sender, args) => changingRaised = true; + linearRange.TypeChanged += (sender, args) => changedRaised = true; + + // Act + linearRange.Type = linearRange.Type; + + // Assert + Assert.False (changingRaised); + Assert.False (changedRaised); + } + + [Fact] + public void Type_PropertyChange_ChangingEventCanModifyNewValue () + { + // Arrange + LinearRange linearRange = new (); + var modifiedValue = LinearRangeType.Multiple; + + linearRange.TypeChanging += (sender, args) => { args.NewValue = modifiedValue; }; + + // Act + linearRange.Type = LinearRangeType.Range; + + // Assert + Assert.Equal (modifiedValue, linearRange.Type); + } +}