namespace Terminal.Gui; /// 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; } } /// 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; } /// 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 fired when the an option has changed. public event EventHandler Changed; /// 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 SliderOptionEventArgs (isSet)); } /// To Raise the event from the Slider. internal void OnSet () { Set?.Invoke (this, new SliderOptionEventArgs (true)); } /// To Raise the event from the Slider. internal void OnUnSet () { UnSet?.Invoke (this, new SliderOptionEventArgs (false)); } } /// Types public enum SliderType { /// /// /// ├─┼─┼─┼─┼─█─┼─┼─┼─┼─┼─┼─┤ /// /// Single, /// /// /// ├─┼─█─┼─┼─█─┼─┼─┼─┼─█─┼─┤ /// /// Multiple, /// /// /// ├▒▒▒▒▒▒▒▒▒█─┼─┼─┼─┼─┼─┼─┤ /// /// LeftRange, /// /// /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒▒▒▒▒▒▒┤ /// /// RightRange, /// /// /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒█─┼─┼─┤ /// /// Range } /// Legend Style public class SliderAttributes { /// Attribute for the Legends Container. public Attribute? EmptyAttribute { get; set; } /// Attribute for when the respective Option is NOT Set. public Attribute? NormalAttribute { get; set; } /// Attribute for when the respective Option is Set. public Attribute? SetAttribute { get; set; } } /// Style public class SliderStyle { /// Constructs a new instance. public SliderStyle () { LegendAttributes = new SliderAttributes (); } /// 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. public Cell EmptyChar { get; set; } /// The glyph and the attribute used for the end of ranges on the slider. public Cell EndRangeChar { get; set; } /// Legend attributes public SliderAttributes LegendAttributes { get; set; } /// The glyph and the attribute used for each option (tick) on the slider. public Cell OptionChar { get; set; } /// The glyph and the attribute used for filling in ranges on the slider. public Cell RangeChar { get; set; } /// The glyph and the attribute used for options (ticks) that are set on the slider. public Cell SetChar { get; set; } /// The glyph and the attribute used for spaces between options (ticks) on the slider. public Cell SpaceChar { get; set; } /// The glyph and the attribute used for the start of ranges on the slider. public Cell StartRangeChar { get; set; } } /// All configuration are grouped in this class. internal class SliderConfiguration { internal bool _allowEmpty; internal bool _autoSize; internal int _endSpacing; internal int _innerSpacing; internal Orientation _legendsOrientation = Orientation.Horizontal; internal bool _rangeAllowSingle; internal bool _showEndSpacing; internal bool _showLegends; internal bool _showLegendsAbbr; internal Orientation _sliderOrientation = Orientation.Horizontal; internal int _startSpacing; internal SliderType _type = SliderType.Single; } /// for events. public class SliderEventArgs : EventArgs { /// 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) { Options = options; Focused = focused; Cancel = false; } /// If set to true, the focus operation will be canceled, if applicable. public bool Cancel { get; set; } /// Gets or sets the index of the option that is focused. public int Focused { get; set; } /// Gets/sets whether the option is set or not. public Dictionary> Options { get; set; } } /// for events. public class OrientationEventArgs : EventArgs { /// Constructs a new instance. /// the new orientation public OrientationEventArgs (Orientation orientation) { Orientation = orientation; Cancel = false; } /// If set to true, the orientation change operation will be canceled, if applicable. public bool Cancel { get; set; } /// The new orientation. public Orientation Orientation { get; set; } } /// Slider control. public class Slider : Slider { /// Initializes a new instance of the class. public Slider () { } /// Initializes a new instance of the class. /// Initial slider options. /// Initial slider options. public Slider (List options, Orientation orientation = Orientation.Horizontal) : base (options, orientation) { } } /// /// Provides a slider control letting the user navigate from a set of typed options in a linear manner using the /// keyboard or mouse. /// /// public class Slider : View { private readonly SliderConfiguration _config = new (); // List of the current set options. private readonly List _setOptions = new (); // Options private List> _options; /// The focused option (has the cursor). public int FocusedOption { get; set; } #region Initialize private void SetInitialProperties ( List> options, Orientation orientation = Orientation.Horizontal ) { CanFocus = true; _options = options ?? new List> (); _config._sliderOrientation = orientation; _config._showLegends = true; SetDefaultStyle (); SetCommands (); // When we lose focus of the View(Slider), if we are range selecting we stop it. Leave += (s, e) => { //if (_settingRange == true) { // _settingRange = false; //} Driver.SetCursorVisibility (CursorVisibility.Invisible); }; Enter += (s, e) => { }; LayoutComplete += (s, e) => { CalcSpacingConfig (); SetBoundsBestFit (); }; } #endregion #region Events /// Event raised when the slider 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. public virtual void OnOptionsChanged () { OptionsChanged?.Invoke (this, new SliderEventArgs (GetSetOptionDictionary ())); SetNeedsDisplay (); } /// Event raised When the option is hovered with the keys or the mouse. public event EventHandler> OptionFocused; private int _lastFocusedOption; // for Range type; the most recently focused option. Used to determine shrink direction /// Overridable function that fires the event. /// /// if the focus change was cancelled. /// public virtual bool OnOptionFocused (int newFocusedOption, SliderEventArgs args) { if (newFocusedOption > _options.Count - 1 || newFocusedOption < 0) { return true; } OptionFocused?.Invoke (this, args); if (!args.Cancel) { _lastFocusedOption = FocusedOption; FocusedOption = newFocusedOption; PositionCursor (); } return args.Cancel; } #endregion #region Constructors /// Initializes a new instance of the class. public Slider () : this (new List ()) { } /// Initializes a new instance of the class. /// Initial slider options. /// Initial slider orientation. public Slider (List options, Orientation orientation = Orientation.Horizontal) { if (options is null) { SetInitialProperties (null, orientation); } else { SetInitialProperties ( options.Select ( e => { var legend = e.ToString (); return new SliderOption { Data = e, Legend = legend, LegendAbbr = (Rune)(legend?.Length > 0 ? legend [0] : ' ') }; } ) .ToList (), orientation ); } } #endregion #region Properties /// Allow no selection. public bool AllowEmpty { get => _config._allowEmpty; set { _config._allowEmpty = value; if (!value && _options.Count > 0 && _setOptions.Count == 0) { SetOption (0); } } } /// /// If the slider will be sized to fit the available space (the Viewport of the the /// SuperView). /// /// /// For testing, if there is no SuperView, the slider will be sized based on what is /// set to. /// public override bool AutoSize { get => _config._autoSize; set { _config._autoSize = value; if (IsInitialized) { CalcSpacingConfig (); SetBoundsBestFit (); } } } /// Gets or sets the number of rows/columns between public int InnerSpacing { get => _config._innerSpacing; set { _config._innerSpacing = value; if (IsInitialized) { CalcSpacingConfig (); SetBoundsBestFit (); } } } /// Slider Type. public SliderType Type { get => _config._type; set { _config._type = value; // Todo: Custom logic to preserve options. _setOptions.Clear (); SetNeedsDisplay (); } } /// Slider Orientation. public Orientation Orientation { get => _config._sliderOrientation; set => OnOrientationChanged (value); } /// /// Fired when the slider orientation has changed. Can be cancelled by setting /// to true. /// public event EventHandler OrientationChanged; /// Called when the slider orientation has changed. Invokes the event. /// /// True of the event was cancelled. public virtual bool OnOrientationChanged (Orientation newOrientation) { var args = new OrientationEventArgs (newOrientation); OrientationChanged?.Invoke (this, args); if (!args.Cancel) { _config._sliderOrientation = newOrientation; SetKeyBindings (); if (IsInitialized) { CalcSpacingConfig (); SetBoundsBestFit (); } } return args.Cancel; } /// Legends Orientation. public Orientation LegendsOrientation { get => _config._legendsOrientation; set { _config._legendsOrientation = value; if (IsInitialized) { CalcSpacingConfig (); SetBoundsBestFit (); } } } /// Slider styles. public SliderStyle Style { // Note(jmperricone): Maybe SliderStyle should be a struct so we return a copy ??? // Or SetStyle() and ( GetStyle() || Style getter copy ) get; set; } = new (); /// Set the slider options. public List> Options { get => // Note(jmperricone): Maybe SliderOption should be a struct so we return a copy ??? // Events will be preserved ? Need a test. // Or SetOptions() and ( GetOptions() || Options getter copy ) _options; set { // _options should never be null _options = value ?? throw new ArgumentNullException (nameof (value)); if (!IsInitialized || _options.Count == 0) { return; } CalcSpacingConfig (); SetBoundsBestFit (); } } /// Allow range start and end be in the same option, as a single option. public bool RangeAllowSingle { get => _config._rangeAllowSingle; set => _config._rangeAllowSingle = value; } /// Show/Hide spacing before and after the first and last option. public bool ShowEndSpacing { get => _config._showEndSpacing; set { _config._showEndSpacing = value; SetNeedsDisplay (); } } /// Show/Hide the options legends. public bool ShowLegends { get => _config._showLegends; set { _config._showLegends = value; SetBoundsBestFit (); } } /// Causes the specified option to be set and be focused. public bool SetOption (int optionIndex) { // 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) { FocusedOption = optionIndex; SetFocusedOption (); return true; } return false; } /// Causes the specified option to be un-set and be focused. public bool UnSetOption (int optionIndex) { // TODO: Handle range type. if (!AllowEmpty && _setOptions.Count > 2 && _setOptions.Contains (optionIndex)) { FocusedOption = optionIndex; SetFocusedOption (); return true; } return false; } /// Get the indexes of the set options. public List GetSetOptions () { // Copy return _setOptions.OrderBy (e => e).ToList (); } #endregion #region Helpers private void MoveAndAdd (int x, int y, Rune rune) { Move (x, y); Driver?.AddRune (rune); } private void MoveAndAdd (int x, int y, string str) { Move (x, y); Driver?.AddStr (str); } // TODO: Make configurable via ConfigurationManager private void SetDefaultStyle () { switch (_config._sliderOrientation) { case Orientation.Horizontal: Style.SpaceChar = new Cell { Rune = Glyphs.HLine }; // '─' Style.OptionChar = new Cell { Rune = Glyphs.BlackCircle }; // '┼●🗹□⏹' break; case Orientation.Vertical: Style.SpaceChar = new Cell { Rune = Glyphs.VLine }; Style.OptionChar = new Cell { Rune = Glyphs.BlackCircle }; break; } // TODO(jmperricone) Wide Vertical ??? /* │ │ ┼─ 40 │ │ ███ 30 ▒▒▒ ▒▒▒ ▒▒▒ 20 ▒▒▒ ▒▒▒ ███ 10 │ │ ─●─ 0 */ _config._legendsOrientation = _config._sliderOrientation; Style.EmptyChar = new Cell { Rune = new Rune (' ') }; Style.SetChar = new Cell { Rune = Glyphs.ContinuousMeterSegment }; // ■ Style.RangeChar = new Cell { Rune = Glyphs.Stipple }; // ░ ▒ ▓ // Medium shade not blinking on curses. Style.StartRangeChar = new Cell { Rune = Glyphs.ContinuousMeterSegment }; Style.EndRangeChar = new Cell { Rune = Glyphs.ContinuousMeterSegment }; Style.DragChar = new Cell { Rune = Glyphs.Diamond }; // TODO: Support left & right (top/bottom) // First = '├', // Last = '┤', } /// /// Calculates the spacing configuration (start, inner, end) as well as turning on/off legend abbreviation if /// needed. Behaves differently based on and . /// internal void CalcSpacingConfig () { var size = 0; if (_options.Count == 0 || !IsInitialized) { return; } _config._innerSpacing = 0; _config._startSpacing = 0; _config._endSpacing = 0; if (AutoSize) { // Max size is SuperView's Viewport. Min Size is size that will fit. if (SuperView is { }) { // Calculate the size of the slider based on the size of the SuperView's Viewport. if (_config._sliderOrientation == Orientation.Horizontal) { size = int.Min (SuperView.Viewport.Width, CalcBestLength ()); } else { size = int.Min (SuperView.Viewport.Height, CalcBestLength ()); } } else { // Use the config values size = CalcMinLength (); return; } } else { // Fit Slider to the Viewport if (_config._sliderOrientation == Orientation.Horizontal) { size = Viewport.Width; } else { size = Viewport.Height; } } int max_legend; // Because the legends are centered, the longest one determines inner spacing if (_config._sliderOrientation == _config._legendsOrientation) { max_legend = int.Max (_options.Max (s => s.Legend?.Length ?? 1), 1); } else { max_legend = 1; } int min_size_that_fits_legends = _options.Count == 1 ? max_legend : max_legend / (_options.Count - 1); string first; string last; if (max_legend >= size) { if (_config._sliderOrientation == _config._legendsOrientation) { _config._showLegendsAbbr = true; foreach (SliderOption o in _options.Where (op => op.LegendAbbr == default (Rune))) { o.LegendAbbr = (Rune)(o.Legend?.Length > 0 ? o.Legend [0] : ' '); } } first = "x"; last = "x"; } else { _config._showLegendsAbbr = false; first = _options.First ().Legend; last = _options.Last ().Legend; } // --o-- // Hello // Left = He // Right = lo int first_left = (first.Length - 1) / 2; // Chars count of the first option to the left. int last_right = last.Length / 2; // Chars count of the last option to the right. if (_config._sliderOrientation != _config._legendsOrientation) { first_left = 0; last_right = 0; } // -1 because it's better to have an extra space at right than to clip int width = size - first_left - last_right - 1; _config._startSpacing = first_left; if (_options.Count == 1) { _config._innerSpacing = max_legend; } else { _config._innerSpacing = Math.Max (0, (int)Math.Floor ((double)width / (_options.Count - 1)) - 1); } _config._endSpacing = last_right; } /// Adjust the dimensions of the Slider to the best value if is true. public void SetBoundsBestFit () { if (!IsInitialized || AutoSize == false) { return; } Thickness adornmentsThickness = GetAdornmentsThickness (); if (_config._sliderOrientation == Orientation.Horizontal) { Viewport = new ( Viewport.Location, new ( int.Min ( SuperView.Viewport.Width - adornmentsThickness.Horizontal, CalcBestLength () ), int.Min ( SuperView.Viewport.Height - adornmentsThickness.Vertical, CalcThickness () ) ) ); } else { Viewport = new ( Viewport.Location, new ( int.Min ( SuperView.Viewport.Width - adornmentsThickness.Horizontal, CalcThickness () ), int.Min ( SuperView.Viewport.Height - adornmentsThickness.Vertical, CalcBestLength () ) ) ); } } /// Calculates the min dimension required for all options and inner spacing with abbreviated legends /// private int CalcMinLength () { if (_options.Count == 0) { return 0; } var length = 0; length += _config._startSpacing + _config._endSpacing; length += _options.Count; length += (_options.Count - 1) * _config._innerSpacing; return length; } /// Calculates the ideal dimension required for all options, inner spacing, and legends (non-abbreviated). /// private int CalcBestLength () { if (_options.Count == 0) { return 0; } var length = 0; if (_config._showLegends) { var max_legend = 1; if (_config._legendsOrientation == _config._sliderOrientation && _options.Count > 0) { max_legend = int.Max (_options.Max (s => s.Legend?.Length + 1 ?? 1), 1); length += max_legend * _options.Count; //length += (max_legend / 2); } else { length += 1; } } return int.Max (length, CalcMinLength ()); } /// Calculates the min dimension required for the slider and legends /// private int CalcThickness () { var thickness = 1; // Always show the slider. if (_config._showLegends) { if (_config._legendsOrientation != _config._sliderOrientation && _options.Count > 0) { thickness += _options.Max (s => s.Legend?.Length ?? 0); } else { thickness += 1; } } return thickness; } internal bool TryGetPositionByOption (int option, out (int x, int y) position) { position = (-1, -1); if (option < 0 || option >= _options.Count ()) { return false; } var offset = 0; offset += _config._startSpacing; offset += option * (_config._innerSpacing + 1); if (_config._sliderOrientation == Orientation.Vertical) { position = (0, offset); } else { position = (offset, 0); } return true; } /// Tries to get the option index by the position. /// /// /// /// /// internal bool TryGetOptionByPosition (int x, int y, int threshold, out int option_idx) { // Fix(jmperricone): Not working. option_idx = -1; if (Orientation == Orientation.Horizontal) { if (y != 0) { return false; } for (int xx = x - threshold; xx < x + threshold + 1; xx++) { int cx = xx; cx -= _config._startSpacing; int option = cx / (_config._innerSpacing + 1); bool valid = cx % (_config._innerSpacing + 1) == 0; if (!valid || option < 0 || option > _options.Count - 1) { continue; } option_idx = option; return true; } } else { if (x != 0) { return false; } for (int yy = y - threshold; yy < y + threshold + 1; yy++) { int cy = yy; cy -= _config._startSpacing; int option = cy / (_config._innerSpacing + 1); bool valid = cy % (_config._innerSpacing + 1) == 0; if (!valid || option < 0 || option > _options.Count - 1) { continue; } option_idx = option; return true; } } return false; } #endregion #region Cursor and Drawing /// public override void PositionCursor () { //base.PositionCursor (); if (HasFocus) { Driver?.SetCursorVisibility (CursorVisibility.Default); } else { Driver?.SetCursorVisibility (CursorVisibility.Invisible); } if (TryGetPositionByOption (FocusedOption, out (int x, int y) position)) { if (IsInitialized && Viewport.Contains (position.x, position.y)) { Move (position.x, position.y); } } } /// public override void OnDrawContent (Rectangle viewport) { // TODO: make this more surgical to reduce repaint if (_options is null && _options.Count > 0) { return; } // Debug #if (DEBUG) Driver?.SetAttribute (new Attribute (Color.White, Color.Red)); for (var y = 0; y < viewport.Height; y++) { for (var x = 0; x < viewport.Width; x++) { // MoveAndAdd (x, y, '·'); } } #endif // Draw Slider DrawSlider (); // Draw Legends. if (_config._showLegends) { DrawLegends (); } if (_dragPosition.HasValue && _moveRenderPosition.HasValue) { AddRune (_moveRenderPosition.Value.X, _moveRenderPosition.Value.Y, Style.DragChar.Rune); } } private string AlignText (string text, int width, TextAlignment textAlignment) { if (text is null) { return ""; } if (text.Length > width) { text = text [..width]; } int w = width - text.Length; string s1 = new (' ', w / 2); string s2 = new (' ', w % 2); // Note: The formatter doesn't handle all of this ??? switch (textAlignment) { case TextAlignment.Justified: return TextFormatter.Justify (text, width); case TextAlignment.Left: return text + s1 + s1 + s2; case TextAlignment.Centered: if (text.Length % 2 != 0) { return s1 + text + s1 + s2; } return s1 + s2 + text + s1; case TextAlignment.Right: return s1 + s1 + s2 + text; default: return text; } } private void DrawSlider () { // TODO: be more surgical on clear Clear (Viewport); // Attributes var normalAttr = new Attribute (Color.White, Color.Black); var setAtrr = new Attribute (Color.Black, Color.White); if (IsInitialized) { normalAttr = ColorScheme?.Normal ?? Application.Current.ColorScheme.Normal; setAtrr = Style.SetChar.Attribute ?? ColorScheme.HotNormal; } bool isVertical = _config._sliderOrientation == Orientation.Vertical; bool isLegendsVertical = _config._legendsOrientation == Orientation.Vertical; bool isReverse = _config._sliderOrientation != _config._legendsOrientation; var x = 0; var y = 0; bool isSet = _setOptions.Count > 0; // Left Spacing if (_config._showEndSpacing && _config._startSpacing > 0) { Driver?.SetAttribute ( isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); Rune rune = isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; for (var i = 0; i < _config._startSpacing; i++) { MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } } } else { Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < _config._startSpacing; i++) { MoveAndAdd (x, y, Style.EmptyChar.Rune); if (isVertical) { y++; } else { x++; } } } // Slider if (_options.Count > 0) { for (var i = 0; i < _options.Count; i++) { var drawRange = false; if (isSet) { switch (_config._type) { case SliderType.LeftRange when i <= _setOptions [0]: drawRange = i < _setOptions [0]; break; case SliderType.RightRange when i >= _setOptions [0]: drawRange = i >= _setOptions [0]; break; case SliderType.Range when _setOptions.Count == 1: drawRange = false; break; case SliderType.Range when _setOptions.Count == 2: if ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0])) { drawRange = (i >= _setOptions [0] && i < _setOptions [1]) || (i >= _setOptions [1] && i < _setOptions [0]); } break; } } // Draw Option Driver?.SetAttribute ( isSet && _setOptions.Contains (i) ? Style.SetChar.Attribute ?? setAtrr : drawRange ? Style.RangeChar.Attribute ?? setAtrr : Style.OptionChar.Attribute ?? normalAttr ); // Note(jmperricone): Maybe only for curses, windows inverts actual colors, while curses inverts bg with fg. //if (Application.Driver is CursesDriver) { // if (FocusedOption == i && HasFocus) { // Driver.SetAttribute (ColorScheme.Focus); // } //} Rune rune = drawRange ? Style.RangeChar.Rune : Style.OptionChar.Rune; if (isSet) { if (_setOptions [0] == i) { rune = Style.StartRangeChar.Rune; } else if (_setOptions.Count > 1 && _setOptions [1] == i) { rune = Style.EndRangeChar.Rune; } else if (_setOptions.Contains (i)) { rune = Style.SetChar.Rune; } } MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } // Draw Spacing if (_config._showEndSpacing || i < _options.Count - 1) { // Skip if is the Last Spacing. Driver?.SetAttribute ( drawRange && isSet ? Style.RangeChar.Attribute ?? setAtrr : Style.SpaceChar.Attribute ?? normalAttr ); for (var s = 0; s < _config._innerSpacing; s++) { MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Rune : Style.SpaceChar.Rune); if (isVertical) { y++; } else { x++; } } } } } int remaining = isVertical ? Viewport.Height - y : Viewport.Width - x; // Right Spacing if (_config._showEndSpacing) { Driver?.SetAttribute ( isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); Rune rune = isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; for (var i = 0; i < remaining; i++) { MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } } } else { Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < remaining; i++) { MoveAndAdd (x, y, Style.EmptyChar.Rune); if (isVertical) { y++; } else { x++; } } } } private void DrawLegends () { // Attributes var normalAttr = new Attribute (Color.White, Color.Black); var setAttr = new Attribute (Color.Black, Color.White); Attribute spaceAttr = normalAttr; if (IsInitialized) { normalAttr = Style.LegendAttributes.NormalAttribute ?? ColorScheme?.Normal ?? ColorScheme.Disabled; setAttr = Style.LegendAttributes.SetAttribute ?? ColorScheme?.HotNormal ?? ColorScheme.Normal; spaceAttr = Style.LegendAttributes.EmptyAttribute ?? normalAttr; } bool isTextVertical = _config._legendsOrientation == Orientation.Vertical; bool isSet = _setOptions.Count > 0; var x = 0; var y = 0; Move (x, y); if (_config._sliderOrientation == Orientation.Horizontal && _config._legendsOrientation == Orientation.Vertical) { x += _config._startSpacing; } if (_config._sliderOrientation == Orientation.Vertical && _config._legendsOrientation == Orientation.Horizontal) { y += _config._startSpacing; } if (_config._sliderOrientation == Orientation.Horizontal) { y += 1; } else { // Vertical x += 1; } 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: if (isSet && _setOptions.Contains (i)) { isOptionSet = true; } break; case SliderType.LeftRange: if (isSet && i <= _setOptions [0]) { isOptionSet = true; } break; case SliderType.RightRange: if (isSet && i >= _setOptions [0]) { isOptionSet = true; } break; case SliderType.Range when _setOptions.Count == 1: if (isSet && i == _setOptions [0]) { isOptionSet = true; } break; case SliderType.Range: if (isSet && ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0]))) { isOptionSet = true; } break; } // 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; } switch (_config._sliderOrientation) { case Orientation.Horizontal: switch (_config._legendsOrientation) { case Orientation.Horizontal: text = AlignText (text, _config._innerSpacing + 1, TextAlignment.Centered); break; case Orientation.Vertical: y = 1; break; } break; case Orientation.Vertical: switch (_config._legendsOrientation) { case Orientation.Horizontal: x = 1; break; case Orientation.Vertical: text = AlignText (text, _config._innerSpacing + 1, TextAlignment.Centered); break; } break; } // Text int legend_left_spaces_count = text.TakeWhile (e => e == ' ').Count (); int legend_right_spaces_count = text.Reverse ().TakeWhile (e => e == ' ').Count (); text = text.Trim (); // TODO(jmperricone): Improve the Orientation check. // Calculate Start Spacing if (_config._sliderOrientation == _config._legendsOrientation) { if (i == 0) { // The spacing for the slider 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. int chars_left = (text.Length - 1) / 2; legend_left_spaces_count = _config._startSpacing - chars_left; } // Option Left Spacing if (isTextVertical) { y += legend_left_spaces_count; } else { x += legend_left_spaces_count; } //Move (x, y); } // Legend Driver?.SetAttribute (isOptionSet ? setAttr : normalAttr); foreach (Rune c in text.EnumerateRunes ()) { MoveAndAdd (x, y, c); //Driver.AddRune (c); if (isTextVertical) { y += 1; } else { x += 1; } } // Calculate End Spacing if (i == _options.Count () - 1) { // See Start Spacing explanation. int chars_right = text.Length / 2; legend_right_spaces_count = _config._endSpacing - chars_right; } // Option Right Spacing of Option Driver?.SetAttribute (spaceAttr); if (isTextVertical) { y += legend_right_spaces_count; } else { x += legend_right_spaces_count; } if (_config._sliderOrientation == Orientation.Horizontal && _config._legendsOrientation == Orientation.Vertical) { x += _config._innerSpacing + 1; } else if (_config._sliderOrientation == Orientation.Vertical && _config._legendsOrientation == Orientation.Horizontal) { y += _config._innerSpacing + 1; } } } #endregion #region Keys and Mouse // Mouse coordinates of current drag private Point? _dragPosition; // Coordinates of where the "move cursor" is drawn (in OnDrawContent) private Point? _moveRenderPosition; /// protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { // Note(jmperricone): Maybe we click to focus the cursor, and on next click we set the option. // That will makes OptionFocused Event more relevant. // (tig: I don't think so. Maybe an option if someone really wants it, but for now that // adss to 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) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Released))) { return false; } 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._innerSpacing; int right = left + width - 1; int clampedX = Clamp (position.X, left, right); position = new Point (clampedX, 0); } else { int top = _config._startSpacing; int height = _options.Count + (_options.Count - 1) * _config._innerSpacing; int bottom = top + height - 1; int clampedY = Clamp (position.Y, top, bottom); position = new Point (0, clampedY); } return position; } SetFocus (); if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { if (mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition)) { _dragPosition = new Point (mouseEvent.X, mouseEvent.Y); _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); Application.GrabMouse (this); } SetNeedsDisplay (); return true; } if (_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition) && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { // Continue Drag _dragPosition = new Point (mouseEvent.X, mouseEvent.Y); _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); var success = false; var option = 0; // how far has user dragged from original location? if (Orientation == Orientation.Horizontal) { success = TryGetOptionByPosition (mouseEvent.X, 0, Math.Max (0, _config._innerSpacing / 2), out option); } else { success = TryGetOptionByPosition (0, mouseEvent.Y, Math.Max (0, _config._innerSpacing / 2), out option); } if (!_config._allowEmpty && success) { if (!OnOptionFocused (option, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { SetFocusedOption (); } } SetNeedsDisplay (); return true; } if ((_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Released)) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // End Drag Application.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.X, 0, Math.Max (0, _config._innerSpacing / 2), out option); } else { success = TryGetOptionByPosition (0, mouseEvent.Y, Math.Max (0, _config._innerSpacing / 2), out option); } if (success) { if (!OnOptionFocused (option, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { SetFocusedOption (); } } SetNeedsDisplay (); return true; } return false; } private void SetCommands () { AddCommand (Command.Right, () => MovePlus ()); AddCommand (Command.LineDown, () => MovePlus ()); AddCommand (Command.Left, () => MoveMinus ()); AddCommand (Command.LineUp, () => MoveMinus ()); AddCommand (Command.LeftHome, () => MoveStart ()); AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.RightExtend, () => ExtendPlus ()); AddCommand (Command.LeftExtend, () => ExtendMinus ()); AddCommand (Command.Accept, () => Set ()); SetKeyBindings (); } // This is called during initialization and anytime orientation changes private void SetKeyBindings () { if (_config._sliderOrientation == Orientation.Horizontal) { KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Remove (Key.CursorDown); KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Remove (Key.CursorUp); KeyBindings.Add (Key.CursorRight.WithCtrl, Command.RightExtend); KeyBindings.Remove (Key.CursorDown.WithCtrl); KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftExtend); KeyBindings.Remove (Key.CursorUp.WithCtrl); } else { KeyBindings.Remove (Key.CursorRight); KeyBindings.Add (Key.CursorDown, Command.LineDown); KeyBindings.Remove (Key.CursorLeft); KeyBindings.Add (Key.CursorUp, Command.LineUp); KeyBindings.Remove (Key.CursorRight.WithCtrl); KeyBindings.Add (Key.CursorDown.WithCtrl, Command.RightExtend); KeyBindings.Remove (Key.CursorLeft.WithCtrl); KeyBindings.Add (Key.CursorUp.WithCtrl, Command.LeftExtend); } KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.Enter, Command.Accept); KeyBindings.Add (Key.Space, Command.Accept); } private Dictionary> GetSetOptionDictionary () { return _setOptions.ToDictionary (e => e, e => _options [e]); } private void SetFocusedOption () { switch (_config._type) { case SliderType.Single: case SliderType.LeftRange: case SliderType.RightRange: if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } _setOptions.Clear (); _options [FocusedOption].OnUnSet (); if (FocusedOption != prev) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } } else { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } // Raise slider changed event. OnOptionsChanged (); break; case SliderType.Multiple: if (_setOptions.Contains (FocusedOption)) { if (!_config._allowEmpty && _setOptions.Count () == 1) { break; } _setOptions.Remove (FocusedOption); _options [FocusedOption].OnUnSet (); } else { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } OnOptionsChanged (); break; case SliderType.Range: if (_config._rangeAllowSingle) { if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } if (FocusedOption == prev) { // un-set _setOptions.Clear (); _options [FocusedOption].OnUnSet (); } else { _setOptions [0] = FocusedOption; _setOptions.Add (prev); _setOptions.Sort (); _options [FocusedOption].OnSet (); } } else if (_setOptions.Count == 0) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } else { // Extend/Shrink if (FocusedOption < _setOptions [0]) { // extend left _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } else if (FocusedOption > _setOptions [1]) { // extend right _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption >= _setOptions [0] && FocusedOption <= _setOptions [1]) { if (FocusedOption < _lastFocusedOption) { // shrink to the left _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption > _lastFocusedOption) { // shrink to the right _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } if (_setOptions.Count > 1 && _setOptions [0] == _setOptions [1]) { _setOptions.Clear (); _setOptions.Add (FocusedOption); } } } } else { if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } _setOptions [0] = FocusedOption; _setOptions.Add (prev); _setOptions.Sort (); _options [FocusedOption].OnSet (); } else if (_setOptions.Count == 0) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); int next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption - 1; _setOptions.Add (next); _options [next].OnSet (); } else { // Extend/Shrink if (FocusedOption < _setOptions [0]) { // extend left _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } else if (FocusedOption > _setOptions [1]) { // extend right _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption >= _setOptions [0] && FocusedOption <= _setOptions [1] && _setOptions [1] - _setOptions [0] > 1) { if (FocusedOption < _lastFocusedOption) { // shrink to the left _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption > _lastFocusedOption) { // shrink to the right _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } } //if (_setOptions.Count > 1 && _setOptions [0] == _setOptions [1]) { // SetFocusedOption (); //} } } // Raise Slider Option Changed Event. OnOptionsChanged (); break; default: throw new ArgumentOutOfRangeException (_config._type.ToString ()); } } internal bool ExtendPlus () { int next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; if (next != FocusedOption && !OnOptionFocused ( next, new SliderEventArgs ( GetSetOptionDictionary (), FocusedOption ) )) { SetFocusedOption (); } 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 () { int prev = FocusedOption > 0 ? FocusedOption - 1 : FocusedOption; if (prev != FocusedOption && !OnOptionFocused ( prev, new SliderEventArgs ( GetSetOptionDictionary (), FocusedOption ) )) { SetFocusedOption (); } return true; } internal bool Set () { SetFocusedOption (); return true; } internal bool MovePlus () { bool cancelled = OnOptionFocused ( FocusedOption + 1, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption) ); if (cancelled) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveMinus () { bool cancelled = OnOptionFocused ( FocusedOption - 1, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption) ); if (cancelled) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveStart () { if (OnOptionFocused (0, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveEnd () { if (OnOptionFocused (_options.Count - 1, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } #endregion }