From f7e0a293c5e3f05874673142dbb53718fa6773ad Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 11 Nov 2024 00:50:32 -0500 Subject: [PATCH] Refactored and went back and forth. Things are working well. Tests not so much --- Terminal.Gui/View/View.ScrollBars.cs | 2 + Terminal.Gui/Views/Scroll/Scroll.cs | 143 ++-- Terminal.Gui/Views/Scroll/ScrollBar.cs | 53 +- Terminal.Gui/Views/Scroll/ScrollSlider.cs | 141 +++- .../CharMap.cs} | 760 +++++------------- .../Scenarios/CharacterMap/CharacterMap.cs | 342 ++++++++ UICatalog/Scenarios/CharacterMap/README.md | 11 + .../Scenarios/CharacterMap/UcdApiClient.cs | 48 ++ .../Scenarios/CharacterMap/UnicodeRange.cs | 101 +++ UICatalog/Scenarios/ScrollBarDemo.cs | 54 +- UICatalog/Scenarios/ScrollDemo.cs | 373 +++++---- UICatalog/Scenarios/TableEditor.cs | 19 +- UnitTests/Views/ScrollBarTests.cs | 59 +- UnitTests/Views/ScrollTests.cs | 88 +- 14 files changed, 1176 insertions(+), 1018 deletions(-) rename UICatalog/Scenarios/{CharacterMap.cs => CharacterMap/CharMap.cs} (56%) create mode 100644 UICatalog/Scenarios/CharacterMap/CharacterMap.cs create mode 100644 UICatalog/Scenarios/CharacterMap/README.md create mode 100644 UICatalog/Scenarios/CharacterMap/UcdApiClient.cs create mode 100644 UICatalog/Scenarios/CharacterMap/UnicodeRange.cs diff --git a/Terminal.Gui/View/View.ScrollBars.cs b/Terminal.Gui/View/View.ScrollBars.cs index d3e56344f..d2a4845ef 100644 --- a/Terminal.Gui/View/View.ScrollBars.cs +++ b/Terminal.Gui/View/View.ScrollBars.cs @@ -130,11 +130,13 @@ public partial class View { if (_verticalScrollBar.IsValueCreated) { + _verticalScrollBar.Value.ViewportDimension = Viewport.Height; _verticalScrollBar.Value.ContentPosition = Viewport.Y; } if (_horizontalScrollBar.IsValueCreated) { + _horizontalScrollBar.Value.ViewportDimension = Viewport.Width; _horizontalScrollBar.Value.ContentPosition = Viewport.X; } }; diff --git a/Terminal.Gui/Views/Scroll/Scroll.cs b/Terminal.Gui/Views/Scroll/Scroll.cs index 3e5bef959..39bc896c6 100644 --- a/Terminal.Gui/Views/Scroll/Scroll.cs +++ b/Terminal.Gui/Views/Scroll/Scroll.cs @@ -24,8 +24,9 @@ public class Scroll : View, IOrientation, IDesignable public Scroll () { _slider = new (); - Add (_slider); - _slider.FrameChanged += OnSliderOnFrameChanged; + base.Add (_slider); + _slider.Scroll += SliderOnScroll; + _slider.PositionChanged += SliderOnPositionChanged; CanFocus = false; @@ -38,6 +39,7 @@ public class Scroll : View, IOrientation, IDesignable OnOrientationChanged (Orientation); } + /// protected override void OnSubviewLayout (LayoutEventArgs args) { @@ -104,12 +106,32 @@ public class Scroll : View, IOrientation, IDesignable set => _slider.ShowPercent = value; } - private int ViewportDimension => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width; + private int? _viewportDimension; + + /// + /// Gets or sets the size of the viewport into the content being scrolled, bounded by . + /// + /// + /// If not explicitly set, will be the appropriate dimension of the Scroll's Frame. + /// + public int ViewportDimension + { + get + { + if (_viewportDimension.HasValue) + { + return _viewportDimension.Value; + } + return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width; + + } + set => _viewportDimension = value; + } private int _size; /// - /// Gets or sets the total size of the content that can be scrolled. + /// Gets or sets the size of the content that can be scrolled. /// public int Size { @@ -135,43 +157,37 @@ public class Scroll : View, IOrientation, IDesignable public event EventHandler>? SizeChanged; #region SliderPosition - private void OnSliderOnFrameChanged (object? sender, EventArgs args) + + private void SliderOnPositionChanged (object? sender, EventArgs e) { if (ViewportDimension == 0) { return; } - int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X; + int calculatedSliderPos = CalculateSliderPosition (_contentPosition); - RaiseSliderPositionChangeEvents (CalculateSliderPosition (_contentPosition), framePos); + ContentPosition = (int)Math.Round ((double)e.CurrentValue / (ViewportDimension - _slider.Size) * (Size - ViewportDimension)); + + RaiseSliderPositionChangeEvents (calculatedSliderPos, e.CurrentValue); + } + + private void SliderOnScroll (object? sender, EventArgs e) + { + if (ViewportDimension == 0) + { + return; + } } /// /// Gets or sets the position of the start of the Scroll slider, within the Viewport. /// - public int SliderPosition + public int GetSliderPosition () => CalculateSliderPosition (_contentPosition); + + private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition) { - get => CalculateSliderPosition (_contentPosition); - set => RaiseSliderPositionChangeEvents (_slider.Position, value); - } - - private void RaiseSliderPositionChangeEvents (int currentSliderPosition, int newSliderPosition) - { - if (/*newSliderPosition > Size - ViewportDimension ||*/ currentSliderPosition == newSliderPosition) - { - return; - } - - if (OnSliderPositionChanging (currentSliderPosition, newSliderPosition)) - { - return; - } - - CancelEventArgs args = new (ref currentSliderPosition, ref newSliderPosition); - SliderPositionChanging?.Invoke (this, args); - - if (args.Cancel) + if (/*newSliderPosition > Size - ViewportDimension ||*/ calculatedSliderPosition == newSliderPosition) { return; } @@ -179,27 +195,14 @@ public class Scroll : View, IOrientation, IDesignable // This sets the slider position and clamps the value _slider.Position = newSliderPosition; - ContentPosition = (int)Math.Round ((double)newSliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension)); - OnSliderPositionChanged (newSliderPosition); SliderPositionChanged?.Invoke (this, new (in newSliderPosition)); } - /// - /// Called when is changing. Return true to cancel the change. - /// - protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; } - - /// - /// Raised when the is changing. Set to - /// to prevent the position from being changed. - /// - public event EventHandler>? SliderPositionChanging; - - /// Called when has changed. + /// Called when the slider position has changed. protected virtual void OnSliderPositionChanged (int position) { } - /// Raised when the has changed. + /// Raised when the slider position has changed. public event EventHandler>? SliderPositionChanged; private int CalculateSliderPosition (int contentPosition) @@ -219,8 +222,17 @@ public class Scroll : View, IOrientation, IDesignable private int _contentPosition; /// - /// Gets or sets the position of the ScrollSlider within the range of 0.... + /// Gets or sets the position of the slider relative to . /// + /// + /// + /// The content position is clamped to 0 and minus . + /// + /// + /// Setting will result in the and + /// events being raised. + /// + /// public int ContentPosition { get => _contentPosition; @@ -231,14 +243,17 @@ public class Scroll : View, IOrientation, IDesignable return; } - RaiseContentPositionChangeEvents (value); + // Clamp the value between 0 and Size - ViewportDimension + int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, Size - ViewportDimension)); + + RaiseContentPositionChangeEvents (newContentPosition); + + _slider.SetPosition (CalculateSliderPosition (_contentPosition)); } } private void RaiseContentPositionChangeEvents (int newContentPosition) { - // Clamp the value between 0 and Size - ViewportDimension - newContentPosition = (int)Math.Clamp (newContentPosition, 0, Math.Max (0, Size - ViewportDimension)); if (OnContentPositionChanging (_contentPosition, newContentPosition)) { @@ -255,8 +270,6 @@ public class Scroll : View, IOrientation, IDesignable _contentPosition = newContentPosition; - SliderPosition = CalculateSliderPosition (_contentPosition); - OnContentPositionChanged (_contentPosition); ContentPositionChanged?.Invoke (this, new (in _contentPosition)); } @@ -291,35 +304,39 @@ public class Scroll : View, IOrientation, IDesignable /// protected override bool OnMouseClick (MouseEventArgs args) { + // Check if the mouse click is a single click if (!args.IsSingleClicked) { return false; } + int sliderCenter; + int distanceFromCenter; + if (Orientation == Orientation.Vertical) { - // If the position is w/in the slider frame ignore - if (args.Position.Y >= _slider.Frame.Y && args.Position.Y < _slider.Frame.Y + _slider.Frame.Height) - { - return false; - } - - SliderPosition = args.Position.Y; + sliderCenter = _slider.Frame.Y + _slider.Frame.Height / 2; + distanceFromCenter = args.Position.Y - sliderCenter; } else { - // If the position is w/in the slider frame ignore - if (args.Position.X >= _slider.Frame.X && args.Position.X < _slider.Frame.X + _slider.Frame.Width) - { - return false; - } - - SliderPosition = args.Position.X; + sliderCenter = _slider.Frame.X + _slider.Frame.Width / 2; + distanceFromCenter = args.Position.X - sliderCenter; } + // Ratio of the distance to the viewport dimension + double ratio = (double)Math.Abs (distanceFromCenter) / ViewportDimension; + // Jump size based on the ratio and the total content size + int jump = (int)Math.Ceiling (ratio * Size); + + // Adjust the content position based on the distance + ContentPosition += distanceFromCenter < 0 ? -jump : jump; + return true; } + + /// /// Gets or sets the amount each mouse hweel event will incremenet/decrement the . /// diff --git a/Terminal.Gui/Views/Scroll/ScrollBar.cs b/Terminal.Gui/Views/Scroll/ScrollBar.cs index a0a8a52c1..3cc48bea4 100644 --- a/Terminal.Gui/Views/Scroll/ScrollBar.cs +++ b/Terminal.Gui/Views/Scroll/ScrollBar.cs @@ -11,9 +11,6 @@ namespace Terminal.Gui; /// and clicking with the mouse to scroll. /// /// -/// -/// indicates the number of rows or columns the Scroll has moved from 0. -/// /// public class ScrollBar : View, IOrientation, IDesignable { @@ -27,7 +24,6 @@ public class ScrollBar : View, IOrientation, IDesignable CanFocus = false; _scroll = new (); - _scroll.SliderPositionChanging += OnScrollOnSliderPositionChanging; _scroll.SliderPositionChanged += OnScrollOnSliderPositionChanged; _scroll.ContentPositionChanging += OnScrollOnContentPositionChanging; _scroll.ContentPositionChanged += OnScrollOnContentPositionChanged; @@ -112,6 +108,8 @@ public class ScrollBar : View, IOrientation, IDesignable Height = 1; } + // Force a layout to ensure _scroll + Layout (); _scroll.Orientation = newOrientation; } @@ -183,43 +181,34 @@ public class ScrollBar : View, IOrientation, IDesignable } /// Gets or sets the position of the slider within the ScrollBar's Viewport. - /// The position. - public int SliderPosition - { - get => _scroll.SliderPosition; - set => _scroll.SliderPosition = value; - } + /// The position. + public int GetSliderPosition () => _scroll.GetSliderPosition (); - private void OnScrollOnSliderPositionChanging (object? sender, CancelEventArgs e) { SliderPositionChanging?.Invoke (this, e); } private void OnScrollOnSliderPositionChanged (object? sender, EventArgs e) { SliderPositionChanged?.Invoke (this, e); } - /// - /// Raised when the is changing. Set to - /// to prevent the position from being changed. - /// - public event EventHandler>? SliderPositionChanging; - - /// Raised when the has changed. + /// Raised when the position of the slider has changed. public event EventHandler>? SliderPositionChanged; - /// /// Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through. /// public int Size { - get - { - // Add two for increment/decrement buttons - return _scroll.Size + 2; - } - set - { - // Remove two for increment/decrement buttons - _scroll.Size = value - 2; - } + get => _scroll.Size; + set => _scroll.Size = value; } + /// + /// Gets or sets the size of the viewport into the content being scrolled, bounded by . + /// + /// + /// If not explicitly set, will be the appropriate dimension of the Scroll's Frame. + /// + public int ViewportDimension + { + get => _scroll.ViewportDimension; + set => _scroll.ViewportDimension = value; + } /// /// Gets or sets the position of the ScrollSlider within the range of 0.... /// @@ -233,12 +222,12 @@ public class ScrollBar : View, IOrientation, IDesignable private void OnScrollOnContentPositionChanged (object? sender, EventArgs e) { ContentPositionChanged?.Invoke (this, e); } /// - /// Raised when the is changing. Set to + /// Raised when the is changing. Set to /// to prevent the position from being changed. /// public event EventHandler>? ContentPositionChanging; - /// Raised when the has changed. + /// Raised when the has changed. public event EventHandler>? ContentPositionChanged; /// Raised when has changed. @@ -320,7 +309,7 @@ public class ScrollBar : View, IOrientation, IDesignable Width = 1; Height = Dim.Fill (); Size = 200; - SliderPosition = 10; + ContentPosition = 10; //ShowPercent = true; return true; } diff --git a/Terminal.Gui/Views/Scroll/ScrollSlider.cs b/Terminal.Gui/Views/Scroll/ScrollSlider.cs index a3e58bedd..c61c9bc04 100644 --- a/Terminal.Gui/Views/Scroll/ScrollSlider.cs +++ b/Terminal.Gui/Views/Scroll/ScrollSlider.cs @@ -1,5 +1,6 @@ #nullable enable +using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui; @@ -39,6 +40,8 @@ public class ScrollSlider : View, IOrientation, IDesignable // Default size is 1 Size = 1; + + FrameChanged += OnFrameChanged; } #region IOrientation members @@ -67,6 +70,7 @@ public class ScrollSlider : View, IOrientation, IDesignable // Reset Position to 0 when changing orientation X = 0; Y = 0; + //Position = 0; // Reset Size to 1 when changing orientation if (Orientation == Orientation.Vertical) @@ -103,7 +107,7 @@ public class ScrollSlider : View, IOrientation, IDesignable set { _showPercent = value; - SetNeedsDraw(); + SetNeedsDraw (); } } @@ -123,11 +127,11 @@ public class ScrollSlider : View, IOrientation, IDesignable { if (Orientation == Orientation.Vertical) { - return Frame.Height; + return Viewport.Height; } else { - return Frame.Width; + return Viewport.Width; } } set @@ -148,38 +152,123 @@ public class ScrollSlider : View, IOrientation, IDesignable } /// - /// Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame. This is a helper that simply gets or sets the X or Y depending on the - /// . The position will be constrained such that the ScrollSlider will not go outside the Viewport of + /// Gets the size of the viewport into the content being scrolled, bounded by . + /// + /// + /// This is the SuperView's Viewport demension. + /// + public int ViewportDimension => Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 0 : SuperView?.Viewport.Width ?? 0; + + private void OnFrameChanged (object? sender, EventArgs e) + { + Position = Orientation == Orientation.Vertical ? e.CurrentValue.Y : e.CurrentValue.X; + } + + private int _position; + + /// + /// Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame. + /// The position will be constrained such that the ScrollSlider will not go outside the Viewport of /// the . /// public int Position { - get - { - if (Orientation == Orientation.Vertical) - { - return Frame.Y; - } - else - { - return Frame.X; - } - } + get => _position; set { - if (Orientation == Orientation.Vertical) + if (_position == value) { - int viewport = Math.Max (1, SuperView?.Viewport.Height ?? 1); - Y = Math.Clamp (value, 0, viewport - Frame.Height); - } - else - { - int viewport = Math.Max (1, SuperView?.Viewport.Width ?? 1); - X = Math.Clamp (value, 0, viewport - Frame.Width); + return; } + + RaisePositionChangeEvents (ClampPosition (value)); + + SetNeedsLayout (); } } + public void SetPosition (int position) + { + _position = ClampPosition (position); + + if (Orientation == Orientation.Vertical) + { + Y = _position; + } + else + { + X = _position; + } + } + + private int ClampPosition (int newPosittion) + { + if (SuperView is null || !IsInitialized) + { + return 1; + } + + if (Orientation == Orientation.Vertical) + { + return Math.Clamp (newPosittion, 0, ViewportDimension - Viewport.Height); + } + else + { + return Math.Clamp (newPosittion, 0, ViewportDimension - Viewport.Width); + } + } + + private void RaisePositionChangeEvents (int newPosition) + { + if (OnPositionChanging (_position, newPosition)) + { + return; + } + + CancelEventArgs args = new (ref _position, ref newPosition); + PositionChanging?.Invoke (this, args); + + if (args.Cancel) + { + return; + } + + int scrollAmount = newPosition -_position; + _position = newPosition; + + OnPositionChanged (_position); + PositionChanged?.Invoke (this, new (in _position)); + + OnScroll (scrollAmount); + Scroll?.Invoke (this, new (in scrollAmount)); + + RaiseSelecting (new CommandContext (Command.Select, null, null, scrollAmount)); + } + + /// + /// Called when is changing. Return true to cancel the change. + /// + protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; } + + /// + /// Raised when the is changing. Set to + /// to prevent the position from being changed. + /// + public event EventHandler>? PositionChanging; + + /// Called when has changed. + protected virtual void OnPositionChanged (int position) { } + + /// Raised when the has changed. + public event EventHandler>? PositionChanged; + + + /// Called when has changed. Indicates how much to scroll. + protected virtual void OnScroll (int scrollAmount) { } + + /// Raised when the has changed. Indicates how much to scroll. + public event EventHandler>? Scroll; + /// protected override bool OnDrawingText () { @@ -240,7 +329,7 @@ public class ScrollSlider : View, IOrientation, IDesignable if (Orientation == Orientation.Vertical) { Y = Frame.Y + offset < 0 - ? 0 + ? 0 : Frame.Y + offset + Frame.Height > superViewDimension ? Math.Max (superViewDimension - Frame.Height, 0) : Frame.Y + offset; @@ -248,7 +337,7 @@ public class ScrollSlider : View, IOrientation, IDesignable else { X = Frame.X + offset < 0 - ? 0 + ? 0 : Frame.X + offset + Frame.Width > superViewDimension ? Math.Max (superViewDimension - Frame.Width, 0) : Frame.X + offset; diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap/CharMap.cs similarity index 56% rename from UICatalog/Scenarios/CharacterMap.cs rename to UICatalog/Scenarios/CharacterMap/CharMap.cs index 4ff95e423..fd5072fb0 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap/CharMap.cs @@ -1,353 +1,38 @@ -#define OTHER_CONTROLS - +#nullable enable using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; -using System.Reflection; using System.Text; using System.Text.Json; -using System.Text.Unicode; -using System.Threading.Tasks; using Terminal.Gui; -using static Terminal.Gui.SpinnerStyle; namespace UICatalog.Scenarios; /// -/// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a -/// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui - -/// Illustrates how to do infinite scrolling +/// A scrollable map of the Unicode codepoints. /// -[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating infinite content, scrolling, and Unicode.")] -[ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Drawing")] -[ScenarioCategory ("Controls")] -[ScenarioCategory ("Layout")] -[ScenarioCategory ("Scrolling")] -public class CharacterMap : Scenario +/// +/// See for details. +/// +public class CharMap : View, IDesignable { - public Label _errorLabel; - private TableView _categoryList; - private CharMap _charMap; - - // Don't create a Window, just return the top-level view - public override void Main () - { - Application.Init (); - - var top = new Window - { - BorderStyle = LineStyle.None - }; - - _charMap = new () - { - X = 0, - Y = 0, - Width = Dim.Fill (Dim.Func (() => _categoryList.Frame.Width)), - Height = Dim.Fill () - }; - top.Add (_charMap); - -#if OTHER_CONTROLS - _charMap.Y = 1; - - var jumpLabel = new Label - { - X = Pos.Right (_charMap) + 1, - Y = Pos.Y (_charMap), - HotKeySpecifier = (Rune)'_', - Text = "_Jump To Code Point:" - }; - top.Add (jumpLabel); - - var jumpEdit = new TextField - { - X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3" - }; - top.Add (jumpEdit); - - _errorLabel = new () - { - X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err" - }; - top.Add (_errorLabel); - - jumpEdit.Accepting += JumpEditOnAccept; - - _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () }; - _categoryList.FullRowSelect = true; - _categoryList.MultiSelect = false; - - //jumpList.Style.ShowHeaders = false; - //jumpList.Style.ShowHorizontalHeaderOverline = false; - //jumpList.Style.ShowHorizontalHeaderUnderline = false; - _categoryList.Style.ShowHorizontalBottomline = true; - - //jumpList.Style.ShowVerticalCellLines = false; - //jumpList.Style.ShowVerticalHeaderLines = false; - _categoryList.Style.AlwaysShowHeaders = true; - - var isDescending = false; - - _categoryList.Table = CreateCategoryTable (0, isDescending); - - // if user clicks the mouse in TableView - _categoryList.MouseClick += (s, e) => - { - _categoryList.ScreenToCell (e.Position, out int? clickedCol); - - if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked)) - { - EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; - string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category; - isDescending = !isDescending; - - _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending); - - table = (EnumerableTableSource)_categoryList.Table; - - _categoryList.SelectedRow = table.Data - .Select ((item, index) => new { item, index }) - .FirstOrDefault (x => x.item.Category == prevSelection) - ?.index - ?? -1; - } - }; - - int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ()); - - _categoryList.Style.ColumnStyles.Add ( - 0, - new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } - ); - _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 }); - _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 }); - - _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4; - - _categoryList.SelectedCellChanged += (s, args) => - { - EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; - _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start; - }; - - top.Add (_categoryList); - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Quit", - $"{Application.QuitKey}", - () => Application.RequestStop () - ) - } - ), - new ( - "_Options", - new [] { CreateMenuShowWidth () } - ) - ] - }; - top.Add (menu); -#endif // OTHER_CONTROLS - - _charMap.SelectedCodePoint = 0; - _charMap.SetFocus (); - - Application.Run (top); - top.Dispose (); - Application.Shutdown (); - - return; - - void JumpEditOnAccept (object sender, CommandEventArgs e) - { - if (jumpEdit.Text.Length == 0) - { - return; - } - - uint result = 0; - - if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u")) - { - try - { - result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber); - } - catch (FormatException) - { - _errorLabel.Text = "Invalid hex value"; - - return; - } - } - else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u")) - { - try - { - result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber); - } - catch (FormatException) - { - _errorLabel.Text = "Invalid hex value"; - - return; - } - } - else - { - try - { - result = uint.Parse (jumpEdit.Text, NumberStyles.Integer); - } - catch (FormatException) - { - _errorLabel.Text = "Invalid value"; - - return; - } - } - - if (result > RuneExtensions.MaxUnicodeCodePoint) - { - _errorLabel.Text = "Beyond maximum codepoint"; - - return; - } - - _errorLabel.Text = $"U+{result:x5}"; - - EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; - - _categoryList.SelectedRow = table.Data - .Select ((item, index) => new { item, index }) - .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result) - ?.index - ?? -1; - _categoryList.EnsureSelectedCellIsVisible (); - - // Ensure the typed glyph is selected - _charMap.SelectedCodePoint = (int)result; - - // Cancel the event to prevent ENTER from being handled elsewhere - e.Cancel = true; - } - } - - private EnumerableTableSource CreateCategoryTable (int sortByColumn, bool descending) - { - Func orderBy; - var categorySort = string.Empty; - var startSort = string.Empty; - var endSort = string.Empty; - - string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString (); - - switch (sortByColumn) - { - case 0: - orderBy = r => r.Category; - categorySort = sortIndicator; - - break; - case 1: - orderBy = r => r.Start; - startSort = sortIndicator; - - break; - case 2: - orderBy = r => r.End; - endSort = sortIndicator; - - break; - default: - throw new ArgumentException ("Invalid column number."); - } - - IOrderedEnumerable sortedRanges = descending - ? UnicodeRange.Ranges.OrderByDescending (orderBy) - : UnicodeRange.Ranges.OrderBy (orderBy); - - return new ( - sortedRanges, - new () - { - { $"Category{categorySort}", s => s.Category }, - { $"Start{startSort}", s => $"{s.Start:x5}" }, - { $"End{endSort}", s => $"{s.End:x5}" } - } - ); - } - - private MenuItem CreateMenuShowWidth () - { - var item = new MenuItem { Title = "_Show Glyph Width" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _charMap?.ShowGlyphWidths; - item.Action += () => { _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked); }; - - return item; - } - - public override List GetDemoKeyStrokes () - { - List keys = new (); - - for (var i = 0; i < 200; i++) - { - keys.Add (Key.CursorDown); - } - - // Category table - keys.Add (Key.Tab.WithShift); - - // Block elements - keys.Add (Key.B); - keys.Add (Key.L); - - keys.Add (Key.Tab); - - for (var i = 0; i < 200; i++) - { - keys.Add (Key.CursorLeft); - } - - return keys; - } -} - -internal class CharMap : View, IDesignable -{ - private const int COLUMN_WIDTH = 3; + private const int COLUMN_WIDTH = 3; // Width of each column of glyphs + private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested private ContextMenu _contextMenu = new (); - private int _rowHeight = 1; - private int _selected; - private int _start; - private readonly ScrollBar _vScrollBar; private readonly ScrollBar _hScrollBar; + /// + /// Initalizes a new instance. + /// public CharMap () { - ColorScheme = Colors.ColorSchemes ["Dialog"]; + base.ColorScheme = Colors.ColorSchemes ["Dialog"]; CanFocus = true; CursorVisibility = CursorVisibility.Default; - //ViewportSettings = ViewportSettings.AllowLocationGreaterThanContentSize; - - SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _maxCodePoint / 16 * _rowHeight + 1)); // +1 for Header - AddCommand ( Command.Up, () => @@ -424,7 +109,7 @@ internal class CharMap : View, IDesignable Command.End, () => { - SelectedCodePoint = _maxCodePoint; + SelectedCodePoint = MAX_CODE_POINT; return true; } @@ -453,7 +138,9 @@ internal class CharMap : View, IDesignable MouseEvent += Handle_MouseEvent; // Add scrollbars - Padding.Thickness = new (0, 0, 1, 0); + Padding!.Thickness = new (0, 0, 1, 0); + + SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + 1)); // +1 for Header _hScrollBar = new () { @@ -463,7 +150,7 @@ internal class CharMap : View, IDesignable Orientation = Orientation.Horizontal, Width = Dim.Fill (1), Size = GetContentSize ().Width - RowLabelWidth, - Increment = COLUMN_WIDTH + Increment = COLUMN_WIDTH, }; _vScrollBar = new () @@ -510,48 +197,45 @@ internal class CharMap : View, IDesignable Padding.Thickness = Padding.Thickness with { Bottom = 0 }; } - _hScrollBar.ContentPosition = Viewport.X; - _vScrollBar.ContentPosition = Viewport.Y; + //_hScrollBar.ContentPosition = Viewport.X; + //_vScrollBar.ContentPosition = Viewport.Y; }; + + SubviewsLaidOut += (sender, args) => + { + //_vScrollBar.ContentPosition = Viewport.Y; + //_hScrollBar.ContentPosition = Viewport.X; + }; } - private void Handle_MouseEvent (object sender, MouseEventArgs e) + private void ScrollToMakeCursorVisible (Point newCursor) { - if (e.Flags == MouseFlags.WheeledDown) + // Adjust vertical scrolling + if (newCursor.Y < 1) // Header is at Y = 0 { - ScrollVertical (1); - _vScrollBar.ContentPosition = Viewport.Y; - e.Handled = true; - - return; + ScrollVertical (newCursor.Y - 1); + } + else if (newCursor.Y >= Viewport.Height) + { + ScrollVertical (newCursor.Y - Viewport.Height + 1); } - if (e.Flags == MouseFlags.WheeledUp) + // Adjust horizontal scrolling + if (newCursor.X < RowLabelWidth + 1) { - ScrollVertical (-1); - _vScrollBar.ContentPosition = Viewport.Y; - e.Handled = true; - - return; + ScrollHorizontal (newCursor.X - (RowLabelWidth + 1)); + } + else if (newCursor.X >= Viewport.Width) + { + ScrollHorizontal (newCursor.X - Viewport.Width + 1); } - if (e.Flags == MouseFlags.WheeledRight) - { - ScrollHorizontal (1); - _hScrollBar.ContentPosition = Viewport.X; - e.Handled = true; - - return; - } - - if (e.Flags == MouseFlags.WheeledLeft) - { - ScrollHorizontal (-1); - _hScrollBar.ContentPosition = Viewport.X; - e.Handled = true; - } + _vScrollBar.ContentPosition = Viewport.Y; + _hScrollBar.ContentPosition = Viewport.X; } + #region Cursor + /// Gets or sets the coordinates of the Cursor based on the SelectedCodePoint in Viewport-relative coordinates public Point Cursor { @@ -565,7 +249,30 @@ internal class CharMap : View, IDesignable set => throw new NotImplementedException (); } - public static int _maxCodePoint = UnicodeRange.Ranges.Max (r => r.End); + public override Point? PositionCursor () + { + if (HasFocus + && Cursor.X >= RowLabelWidth + && Cursor.X < Viewport.Width + && Cursor.Y > 0 + && Cursor.Y < Viewport.Height) + { + Move (Cursor.X, Cursor.Y); + } + else + { + return null; + } + + return Cursor; + } + + #endregion Cursor + + // ReSharper disable once InconsistentNaming + private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End); + private int _selectedCodepoint; // Currently selected codepoint + private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport /// /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point @@ -573,16 +280,15 @@ internal class CharMap : View, IDesignable /// public int SelectedCodePoint { - get => _selected; + get => _selectedCodepoint; set { - if (_selected == value) + if (_selectedCodepoint == value) { return; } - Point prevCursor = Cursor; - int newSelectedCodePoint = Math.Clamp (value, 0, _maxCodePoint); + int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT); Point newCursor = new () { @@ -590,42 +296,38 @@ internal class CharMap : View, IDesignable Y = newSelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y }; + _selectedCodepoint = newSelectedCodePoint; + // Ensure the new cursor position is visible - EnsureCursorIsVisible (newCursor); + ScrollToMakeCursorVisible (newCursor); - _selected = newSelectedCodePoint; SetNeedsDraw (); - SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null)); + SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint)); } } - private void EnsureCursorIsVisible (Point newCursor) + /// + /// Raised when the selected code point changes. + /// + public event EventHandler>? SelectedCodePointChanged; + + /// + /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing + /// characters. + /// + public int StartCodePoint { - // Adjust vertical scrolling - if (newCursor.Y < 1) // Header is at Y = 0 + get => _startCodepoint; + set { - ScrollVertical (newCursor.Y - 1); + _startCodepoint = value; + SelectedCodePoint = value; } - else if (newCursor.Y >= Viewport.Height) - { - ScrollVertical (newCursor.Y - Viewport.Height + 1); - } - - _vScrollBar.ContentPosition = Viewport.Y; - - // Adjust horizontal scrolling - if (newCursor.X < RowLabelWidth + 1) - { - ScrollHorizontal (newCursor.X - (RowLabelWidth + 1)); - } - else if (newCursor.X >= Viewport.Width) - { - ScrollHorizontal (newCursor.X - Viewport.Width + 1); - } - - _hScrollBar.ContentPosition = Viewport.X; } + /// + /// Gets or sets whether the number of columns each glyph is displayed. + /// public bool ShowGlyphWidths { get => _rowHeight == 2; @@ -636,25 +338,12 @@ internal class CharMap : View, IDesignable } } - /// - /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing - /// characters. - /// - public int StartCodePoint - { - get => _start; - set - { - _start = value; - SelectedCodePoint = value; - Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight }; - SetNeedsDraw (); - } - } + private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; } + private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; } - private static int RowLabelWidth => $"U+{_maxCodePoint:x5}".Length + 1; - private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16; - public event EventHandler Hover; + #region Drawing + + private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1; protected override bool OnDrawingContent () { @@ -682,7 +371,7 @@ internal class CharMap : View, IDesignable Move (x, 0); SetAttribute (GetHotNormalColor ()); AddStr (" "); - SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ()); + SetAttribute (HasFocus && cursorCol + firstColumnX == x ? GetHotFocusColor () : GetHotNormalColor ()); AddStr ($"{hexDigit:x}"); SetAttribute (GetHotNormalColor ()); AddStr (" "); @@ -697,7 +386,7 @@ internal class CharMap : View, IDesignable int val = row * 16; - if (val > _maxCodePoint) + if (val > MAX_CODE_POINT) { break; } @@ -770,7 +459,7 @@ internal class CharMap : View, IDesignable else { // Draw the width of the rune - SetAttribute (ColorScheme.HotNormal); + SetAttribute (GetHotNormalColor ()); AddStr ($"{width}"); } @@ -784,7 +473,7 @@ internal class CharMap : View, IDesignable // Draw row label (U+XXXX_) Move (0, y); - SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal); + SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? GetHotFocusColor () : GetHotNormalColor ()); if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0) { @@ -799,26 +488,11 @@ internal class CharMap : View, IDesignable return true; } - public override Point? PositionCursor () - { - if (HasFocus - && Cursor.X >= RowLabelWidth - && Cursor.X < Viewport.Width - && Cursor.Y > 0 - && Cursor.Y < Viewport.Height) - { - Move (Cursor.X, Cursor.Y); - } - else - { - return null; - } - - return Cursor; - } - - public event EventHandler SelectedCodePointChanged; - + /// + /// Helper to convert a string into camel case. + /// + /// + /// public static string ToCamelCase (string str) { if (string.IsNullOrEmpty (str)) @@ -834,10 +508,51 @@ internal class CharMap : View, IDesignable return str; } - private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; } - private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; } + #endregion Drawing - private void Handle_MouseClick (object sender, MouseEventArgs me) + #region Mouse Handling + + // TODO: Use this to demonstrate using a popover to show glyph info on hover + public event EventHandler? Hover; + + private void Handle_MouseEvent (object? sender, MouseEventArgs e) + { + if (e.Flags == MouseFlags.WheeledDown) + { + ScrollVertical (1); + _vScrollBar.ContentPosition = Viewport.Y; + e.Handled = true; + + return; + } + + if (e.Flags == MouseFlags.WheeledUp) + { + ScrollVertical (-1); + _vScrollBar.ContentPosition = Viewport.Y; + e.Handled = true; + + return; + } + + if (e.Flags == MouseFlags.WheeledRight) + { + ScrollHorizontal (1); + _hScrollBar.ContentPosition = Viewport.X; + e.Handled = true; + + return; + } + + if (e.Flags == MouseFlags.WheeledLeft) + { + ScrollHorizontal (-1); + _hScrollBar.ContentPosition = Viewport.X; + e.Handled = true; + } + } + + private void Handle_MouseClick (object? sender, MouseEventArgs me) { if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked) { @@ -864,7 +579,7 @@ internal class CharMap : View, IDesignable int val = row * 16 + col; - if (val > _maxCodePoint) + if (val > MAX_CODE_POINT) { return; } @@ -931,32 +646,41 @@ internal class CharMap : View, IDesignable } } + #endregion Mouse Handling + + #region Details Dialog + private void ShowDetails () { - var client = new UcdApiClient (); + UcdApiClient? client = new (); var decResponse = string.Empty; var getCodePointError = string.Empty; - var waitIndicator = new Dialog + Dialog? waitIndicator = new () { Title = "Getting Code Point Information", X = Pos.Center (), Y = Pos.Center (), - Height = 7, - Width = 50, - Buttons = [new () { Text = "Cancel" }] + Width = 40, + Height = 10, + Buttons = [new () { Text = "_Cancel" }] }; var errorLabel = new Label { Text = UcdApiClient.BaseUrl, X = 0, - Y = 1, + Y = 0, Width = Dim.Fill (), - Height = Dim.Fill (1), + Height = Dim.Fill (3), TextAlignment = Alignment.Center }; - var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () }; + var spinner = new SpinnerView + { + X = Pos.Center (), + Y = Pos.Bottom (errorLabel), + Style = new SpinnerStyle.Aesthetic () + }; spinner.AutoSpin = true; waitIndicator.Add (errorLabel); waitIndicator.Add (spinner); @@ -1000,30 +724,30 @@ internal class CharMap : View, IDesignable document.RootElement, new JsonSerializerOptions - { WriteIndented = true } + { WriteIndented = true } ); } - var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}"; + var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}"; - var copyGlyph = new Button { Text = "Copy _Glyph" }; - var copyCP = new Button { Text = "Copy Code _Point" }; - var cancel = new Button { Text = "Cancel" }; + Button? copyGlyph = new () { Text = "Copy _Glyph" }; + Button? copyCodepoint = new () { Text = "Copy Code _Point" }; + Button? cancel = new () { Text = "Cancel" }; - var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] }; + var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] }; copyGlyph.Accepting += (s, a) => { CopyGlyph (); - dlg.RequestStop (); + dlg!.RequestStop (); }; - copyCP.Accepting += (s, a) => - { - CopyCodePoint (); - dlg.RequestStop (); - }; - cancel.Accepting += (s, a) => dlg.RequestStop (); + copyCodepoint.Accepting += (s, a) => + { + CopyCodePoint (); + dlg!.RequestStop (); + }; + cancel.Accepting += (s, a) => dlg!.RequestStop (); var rune = (Rune)SelectedCodePoint; var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 }; @@ -1097,129 +821,13 @@ internal class CharMap : View, IDesignable MessageBox.ErrorQuery ( "Code Point API", $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.", - "Ok" + "_Ok" ); } // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug Application.GrabMouse (this); } -} - -public class UcdApiClient -{ - public const string BaseUrl = "https://ucdapi.org/unicode/latest/"; - private static readonly HttpClient _httpClient = new (); - - public async Task GetChars (string chars) - { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}"); - response.EnsureSuccessStatusCode (); - - return await response.Content.ReadAsStringAsync (); - } - - public async Task GetCharsName (string chars) - { - HttpResponseMessage response = - await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name"); - response.EnsureSuccessStatusCode (); - - return await response.Content.ReadAsStringAsync (); - } - - public async Task GetCodepointDec (int dec) - { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}"); - response.EnsureSuccessStatusCode (); - - return await response.Content.ReadAsStringAsync (); - } - - public async Task GetCodepointHex (string hex) - { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}"); - response.EnsureSuccessStatusCode (); - - return await response.Content.ReadAsStringAsync (); - } -} - -internal class UnicodeRange -{ - public static List Ranges = GetRanges (); - - public string Category; - public int End; - public int Start; - - public UnicodeRange (int start, int end, string category) - { - Start = start; - End = end; - Category = category; - } - - public static List GetRanges () - { - IEnumerable ranges = - from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public) - let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange - let name = string.IsNullOrEmpty (r.Name) - ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}" - : r.Name - where name != "None" && name != "All" - select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name); - - // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0 - List nonBmpRanges = new () - { - new ( - 0x1F130, - 0x1F149, - "Squared Latin Capital Letters" - ), - new ( - 0x12400, - 0x1240f, - "Cuneiform Numbers and Punctuation" - ), - new (0x10000, 0x1007F, "Linear B Syllabary"), - new (0x10080, 0x100FF, "Linear B Ideograms"), - new (0x10100, 0x1013F, "Aegean Numbers"), - new (0x10300, 0x1032F, "Old Italic"), - new (0x10330, 0x1034F, "Gothic"), - new (0x10380, 0x1039F, "Ugaritic"), - new (0x10400, 0x1044F, "Deseret"), - new (0x10450, 0x1047F, "Shavian"), - new (0x10480, 0x104AF, "Osmanya"), - new (0x10800, 0x1083F, "Cypriot Syllabary"), - new ( - 0x1D000, - 0x1D0FF, - "Byzantine Musical Symbols" - ), - new (0x1D100, 0x1D1FF, "Musical Symbols"), - new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"), - new ( - 0x1D400, - 0x1D7FF, - "Mathematical Alphanumeric Symbols" - ), - new (0x1F600, 0x1F532, "Emojis Symbols"), - new ( - 0x20000, - 0x2A6DF, - "CJK Unified Ideographs Extension B" - ), - new ( - 0x2F800, - 0x2FA1F, - "CJK Compatibility Ideographs Supplement" - ), - new (0xE0000, 0xE007F, "Tags") - }; - - return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList (); - } + + #endregion Details Dialog } diff --git a/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs new file mode 100644 index 000000000..3456ade77 --- /dev/null +++ b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -0,0 +1,342 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +/// +/// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a +/// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui - +/// Illustrates how to do infinite scrolling +/// +/// +/// See . +/// +[ScenarioMetadata ("Character Map", "Unicode viewer. Demos infinite content drawing and scrolling.")] +[ScenarioCategory ("Text and Formatting")] +[ScenarioCategory ("Drawing")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Layout")] +[ScenarioCategory ("Scrolling")] +public class CharacterMap : Scenario +{ + private Label? _errorLabel; + private TableView? _categoryList; + private CharMap? _charMap; + + // Don't create a Window, just return the top-level view + public override void Main () + { + Application.Init (); + + var top = new Window + { + BorderStyle = LineStyle.None + }; + + _charMap = new () + { + X = 0, + Y = 1, + Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)), + Height = Dim.Fill () + }; + top.Add (_charMap); + + var jumpLabel = new Label + { + X = Pos.Right (_charMap) + 1, + Y = Pos.Y (_charMap), + HotKeySpecifier = (Rune)'_', + Text = "_Jump To:" + }; + top.Add (jumpLabel); + + var jumpEdit = new TextField + { + X = Pos.Right (jumpLabel) + 1, + Y = Pos.Y (_charMap), + Width = 17, + Caption = "e.g. 01BE3 or ✈", + }; + top.Add (jumpEdit); + + _charMap.SelectedCodePointChanged += (sender, args) => jumpEdit.Text = ((Rune)args.CurrentValue).ToString (); + + _errorLabel = new () + { + X = Pos.Right (jumpEdit) + 1, + Y = Pos.Y (_charMap), + ColorScheme = Colors.ColorSchemes ["error"], + Text = "err", + Visible = false + + }; + top.Add (_errorLabel); + + jumpEdit.Accepting += JumpEditOnAccept; + + _categoryList = new () { + X = Pos.Right (_charMap), + Y = Pos.Bottom (jumpLabel), + Height = Dim.Fill (), + }; + _categoryList.FullRowSelect = true; + _categoryList.MultiSelect = false; + + _categoryList.Style.ShowVerticalCellLines = false; + _categoryList.Style.AlwaysShowHeaders = true; + + var isDescending = false; + + _categoryList.Table = CreateCategoryTable (0, isDescending); + + // if user clicks the mouse in TableView + _categoryList.MouseClick += (s, e) => + { + _categoryList.ScreenToCell (e.Position, out int? clickedCol); + + if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked)) + { + EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; + string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category; + isDescending = !isDescending; + + _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending); + + table = (EnumerableTableSource)_categoryList.Table; + + _categoryList.SelectedRow = table.Data + .Select ((item, index) => new { item, index }) + .FirstOrDefault (x => x.item.Category == prevSelection) + ?.index + ?? -1; + } + }; + + int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ()); + + _categoryList.Style.ColumnStyles.Add ( + 0, + new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } + ); + _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 }); + _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 }); + + _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4; + + _categoryList.SelectedCellChanged += (s, args) => + { + EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; + _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start; + jumpEdit.Text = $"U+{_charMap.SelectedCodePoint:x5}"; + }; + + top.Add (_categoryList); + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ( + "_Quit", + $"{Application.QuitKey}", + () => Application.RequestStop () + ) + } + ), + new ( + "_Options", + new [] { CreateMenuShowWidth () } + ) + ] + }; + top.Add (menu); + + _charMap.SelectedCodePoint = 0; + _charMap.SetFocus (); + + Application.Run (top); + top.Dispose (); + Application.Shutdown (); + + return; + + void JumpEditOnAccept (object? sender, CommandEventArgs e) + { + if (jumpEdit.Text.Length == 0) + { + return; + } + + _errorLabel.Visible = true; + + uint result = 0; + + if (jumpEdit.Text.Length == 1) + { + result = (uint)jumpEdit.Text.ToRunes () [0].Value; + } + else if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u")) + { + try + { + result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber); + } + catch (FormatException) + { + _errorLabel.Text = "Invalid hex value"; + + return; + } + } + else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u")) + { + try + { + result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber); + } + catch (FormatException) + { + _errorLabel.Text = "Invalid hex value"; + + return; + } + } + else + { + try + { + result = uint.Parse (jumpEdit.Text, NumberStyles.Integer); + } + catch (FormatException) + { + _errorLabel.Text = "Invalid value"; + + return; + } + } + + if (result > RuneExtensions.MaxUnicodeCodePoint) + { + _errorLabel.Text = "Beyond maximum codepoint"; + + return; + } + + _errorLabel.Visible = false; + + EnumerableTableSource table = (EnumerableTableSource)_categoryList!.Table; + + _categoryList.SelectedRow = table.Data + .Select ((item, index) => new { item, index }) + .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result) + ?.index + ?? -1; + _categoryList.EnsureSelectedCellIsVisible (); + + // Ensure the typed glyph is selected + _charMap.SelectedCodePoint = (int)result; + _charMap.SetFocus (); + + // Cancel the event to prevent ENTER from being handled elsewhere + e.Cancel = true; + } + } + + private EnumerableTableSource CreateCategoryTable (int sortByColumn, bool descending) + { + Func orderBy; + var categorySort = string.Empty; + var startSort = string.Empty; + var endSort = string.Empty; + + string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString (); + + switch (sortByColumn) + { + case 0: + orderBy = r => r.Category; + categorySort = sortIndicator; + + break; + case 1: + orderBy = r => r.Start; + startSort = sortIndicator; + + break; + case 2: + orderBy = r => r.End; + endSort = sortIndicator; + + break; + default: + throw new ArgumentException ("Invalid column number."); + } + + IOrderedEnumerable sortedRanges = descending + ? UnicodeRange.Ranges.OrderByDescending (orderBy) + : UnicodeRange.Ranges.OrderBy (orderBy); + + return new ( + sortedRanges, + new () + { + { $"Category{categorySort}", s => s.Category }, + { $"Start{startSort}", s => $"{s.Start:x5}" }, + { $"End{endSort}", s => $"{s.End:x5}" } + } + ); + } + + private MenuItem CreateMenuShowWidth () + { + var item = new MenuItem { Title = "_Show Glyph Width" }; + item.CheckType |= MenuItemCheckStyle.Checked; + item.Checked = _charMap?.ShowGlyphWidths; + item.Action += () => + { + if (_charMap is { }) + { + _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!; + } + }; + + return item; + } + + public override List GetDemoKeyStrokes () + { + List keys = new (); + + for (var i = 0; i < 200; i++) + { + keys.Add (Key.CursorDown); + } + + // Category table + keys.Add (Key.Tab.WithShift); + + // Block elements + keys.Add (Key.B); + keys.Add (Key.L); + + keys.Add (Key.Tab); + + for (var i = 0; i < 200; i++) + { + keys.Add (Key.CursorLeft); + } + + return keys; + } +} diff --git a/UICatalog/Scenarios/CharacterMap/README.md b/UICatalog/Scenarios/CharacterMap/README.md new file mode 100644 index 000000000..2901e9bf5 --- /dev/null +++ b/UICatalog/Scenarios/CharacterMap/README.md @@ -0,0 +1,11 @@ +# CharacterMap Scenario Deep Dive + +The CharacterMap Scenario is an example of the following Terminal.Gui concepts: + +- **Complex and High-Performnt Drawing** - +- **Virtual Content Scrolling** - +- **Advanced ScrollBar Control** - +- **Unicode wide-glyph Rendering** - +- **Advanced Layout** - +- **Cursor Management** - +- **Context Menu** - \ No newline at end of file diff --git a/UICatalog/Scenarios/CharacterMap/UcdApiClient.cs b/UICatalog/Scenarios/CharacterMap/UcdApiClient.cs new file mode 100644 index 000000000..658a59c3c --- /dev/null +++ b/UICatalog/Scenarios/CharacterMap/UcdApiClient.cs @@ -0,0 +1,48 @@ +#nullable enable +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace UICatalog.Scenarios; + +/// +/// A helper class for accessing the ucdapi.org API. +/// +public class UcdApiClient +{ + public const string BaseUrl = "https://ucdapi.org/unicode/latest/"; + private static readonly HttpClient _httpClient = new (); + + public async Task GetChars (string chars) + { + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}"); + response.EnsureSuccessStatusCode (); + + return await response.Content.ReadAsStringAsync (); + } + + public async Task GetCharsName (string chars) + { + HttpResponseMessage response = + await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name"); + response.EnsureSuccessStatusCode (); + + return await response.Content.ReadAsStringAsync (); + } + + public async Task GetCodepointDec (int dec) + { + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}"); + response.EnsureSuccessStatusCode (); + + return await response.Content.ReadAsStringAsync (); + } + + public async Task GetCodepointHex (string hex) + { + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}"); + response.EnsureSuccessStatusCode (); + + return await response.Content.ReadAsStringAsync (); + } +} diff --git a/UICatalog/Scenarios/CharacterMap/UnicodeRange.cs b/UICatalog/Scenarios/CharacterMap/UnicodeRange.cs new file mode 100644 index 000000000..686823486 --- /dev/null +++ b/UICatalog/Scenarios/CharacterMap/UnicodeRange.cs @@ -0,0 +1,101 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Unicode; + +namespace UICatalog.Scenarios; + +/// +/// Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus +/// the non-BMP ranges not included. +/// +public class UnicodeRange (int start, int end, string category) +{ + /// + /// Gets the list of all ranges. + /// + public static List Ranges => GetRanges (); + + /// + /// The category. + /// + public string Category { get; set; } = category; + + /// + /// Te codepoint at the start of the range. + /// + public int Start { get; set; } = start; + + /// + /// The codepoint at the end of the range. + /// + public int End { get; set; } = end; + + /// + /// Gets the list of all ranges.. + /// + /// + public static List GetRanges () + { + IEnumerable ranges = + from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public) + let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange + let name = string.IsNullOrEmpty (r.Name) + ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}" + : r.Name + where name != "None" && name != "All" + select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name); + + // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0 + List nonBmpRanges = new () + { + new ( + 0x1F130, + 0x1F149, + "Squared Latin Capital Letters" + ), + new ( + 0x12400, + 0x1240f, + "Cuneiform Numbers and Punctuation" + ), + new (0x10000, 0x1007F, "Linear B Syllabary"), + new (0x10080, 0x100FF, "Linear B Ideograms"), + new (0x10100, 0x1013F, "Aegean Numbers"), + new (0x10300, 0x1032F, "Old Italic"), + new (0x10330, 0x1034F, "Gothic"), + new (0x10380, 0x1039F, "Ugaritic"), + new (0x10400, 0x1044F, "Deseret"), + new (0x10450, 0x1047F, "Shavian"), + new (0x10480, 0x104AF, "Osmanya"), + new (0x10800, 0x1083F, "Cypriot Syllabary"), + new ( + 0x1D000, + 0x1D0FF, + "Byzantine Musical Symbols" + ), + new (0x1D100, 0x1D1FF, "Musical Symbols"), + new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"), + new ( + 0x1D400, + 0x1D7FF, + "Mathematical Alphanumeric Symbols" + ), + new (0x1F600, 0x1F532, "Emojis Symbols"), + new ( + 0x20000, + 0x2A6DF, + "CJK Unified Ideographs Extension B" + ), + new ( + 0x2F800, + 0x2FA1F, + "CJK Compatibility Ideographs Supplement" + ), + new (0xE0000, 0xE007F, "Tags") + }; + + return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList (); + } +} diff --git a/UICatalog/Scenarios/ScrollBarDemo.cs b/UICatalog/Scenarios/ScrollBarDemo.cs index dc13b91c1..ca8d2f5ed 100644 --- a/UICatalog/Scenarios/ScrollBarDemo.cs +++ b/UICatalog/Scenarios/ScrollBarDemo.cs @@ -4,12 +4,10 @@ using Terminal.Gui; namespace UICatalog.Scenarios; -[ScenarioMetadata ("ScrollBar Demo", "Demonstrates using ScrollBar view.")] +[ScenarioMetadata ("ScrollBar Demo", "Demonstrates ScrollBar.")] [ScenarioCategory ("Scrolling")] public class ScrollBarDemo : Scenario { - private ViewDiagnosticFlags _diagnosticFlags; - public override void Main () { Application.Init (); @@ -29,23 +27,22 @@ public class ScrollBarDemo : Scenario X = Pos.Right (editor), Width = Dim.Fill (), Height = Dim.Fill (), - ColorScheme = Colors.ColorSchemes ["Base"] + ColorScheme = Colors.ColorSchemes ["Base"], + Arrangement = ViewArrangement.Resizable }; + frameView!.Padding!.Thickness = new (1); + frameView.Padding.Diagnostics = ViewDiagnosticFlags.Ruler; app.Add (frameView); var scrollBar = new ScrollBar { X = Pos.AnchorEnd (), - AutoHide = false + AutoHide = false, + Size = 100, //ShowPercent = true }; frameView.Add (scrollBar); - app.Loaded += (s, e) => - { - scrollBar.Size = scrollBar.Viewport.Height; - }; - int GetMaxLabelWidth (int groupId) { return frameView.Subviews.Max ( @@ -134,7 +131,6 @@ public class ScrollBarDemo : Scenario scrollBar.Y = 0; scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Width); scrollBar.Width = scrollWidthHeight.Value; - scrollBar.Size /= 3; } else { @@ -143,7 +139,6 @@ public class ScrollBarDemo : Scenario scrollBar.Y = Pos.AnchorEnd (); scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Height); scrollBar.Height = scrollWidthHeight.Value; - scrollBar.Size *= 3; } }; @@ -189,34 +184,14 @@ public class ScrollBarDemo : Scenario }; frameView.Add (lblSliderPosition); - NumericUpDown scrollSliderPosition = new () + Label scrollSliderPosition = new () { - Value = scrollBar.SliderPosition, + Text = scrollBar.GetSliderPosition ().ToString (), X = Pos.Right (lblSliderPosition) + 1, Y = Pos.Top (lblSliderPosition) }; frameView.Add (scrollSliderPosition); - scrollSliderPosition.ValueChanging += (s, e) => - { - if (e.NewValue < 0) - { - e.Cancel = true; - - return; - } - - if (scrollBar.SliderPosition != e.NewValue) - { - scrollBar.SliderPosition = e.NewValue; - } - - if (scrollBar.SliderPosition != e.NewValue) - { - e.Cancel = true; - } - }; - var lblContentPosition = new Label { Text = "_ContentPosition:", @@ -229,7 +204,7 @@ public class ScrollBarDemo : Scenario NumericUpDown scrollContentPosition = new () { - Value = scrollBar.SliderPosition, + Value = scrollBar.GetSliderPosition (), X = Pos.Right (lblContentPosition) + 1, Y = Pos.Top (lblContentPosition) }; @@ -342,22 +317,15 @@ public class ScrollBarDemo : Scenario } }; - scrollBar.SliderPositionChanging += (s, e) => - { - eventLog.Log ($"SliderPositionChanging: {e.CurrentValue}"); - eventLog.Log ($" NewValue: {e.NewValue}"); - }; - scrollBar.SliderPositionChanged += (s, e) => { eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}"); eventLog.Log ($" ContentPosition: {scrollBar.ContentPosition}"); - scrollSliderPosition.Value = e.CurrentValue; + scrollSliderPosition.Text = e.CurrentValue.ToString (); }; editor.Initialized += (s, e) => { - scrollBar.Size = int.Max (app.GetContentSize ().Height * 2, app.GetContentSize ().Width * 2); editor.ViewToEdit = scrollBar; }; diff --git a/UICatalog/Scenarios/ScrollDemo.cs b/UICatalog/Scenarios/ScrollDemo.cs index fd96173f6..40c3211ea 100644 --- a/UICatalog/Scenarios/ScrollDemo.cs +++ b/UICatalog/Scenarios/ScrollDemo.cs @@ -1,22 +1,21 @@ using System; +using System.Linq; using Terminal.Gui; namespace UICatalog.Scenarios; -[ScenarioMetadata ("Scroll Demo", "Demonstrates using Scroll view.")] -[ScenarioCategory ("Drawing")] +[ScenarioMetadata ("Scroll Demo", "Demonstrates Scroll.")] [ScenarioCategory ("Scrolling")] public class ScrollDemo : Scenario { - private ViewDiagnosticFlags _diagnosticFlags; - public override void Main () { Application.Init (); Window app = new () { - Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" + Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}", + Arrangement = ViewArrangement.Fixed }; var editor = new AdornmentsEditor (); @@ -28,25 +27,40 @@ public class ScrollDemo : Scenario X = Pos.Right (editor), Width = Dim.Fill (), Height = Dim.Fill (), - ColorScheme = Colors.ColorSchemes ["Base"] + ColorScheme = Colors.ColorSchemes ["Base"], + Arrangement = ViewArrangement.Resizable }; + frameView.Padding.Thickness = new (1); + frameView.Padding.Diagnostics = ViewDiagnosticFlags.Ruler; app.Add (frameView); var scroll = new Scroll { X = Pos.AnchorEnd (), - ShowPercent = true + Size = 1000, }; frameView.Add (scroll); - app.Loaded += (s, e) => - { - scroll.Size = frameView.Viewport.Height; - }; + int GetMaxLabelWidth (int groupId) + { + return frameView.Subviews.Max ( + v => + { + if (v.Y.Has (out var pos) && pos.GroupId == groupId) + { + return v.Text.GetColumns (); + } + + return 0; + }); + } var lblWidthHeight = new Label { - Text = "Width/Height:" + Text = "_Width/Height:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, AlignmentModes.StartToEnd, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) }; frameView.Add (lblWidthHeight); @@ -59,71 +73,79 @@ public class ScrollDemo : Scenario frameView.Add (scrollWidthHeight); scrollWidthHeight.ValueChanging += (s, e) => - { - if (e.NewValue < 1 - || (e.NewValue - > (scroll.Orientation == Orientation.Vertical - ? scroll.SuperView?.GetContentSize ().Width - : scroll.SuperView?.GetContentSize ().Height))) - { - // TODO: This must be handled in the ScrollSlider if Width and Height being virtual - e.Cancel = true; + { + if (e.NewValue < 1 + || (e.NewValue + > (scroll.Orientation == Orientation.Vertical + ? scroll.SuperView?.GetContentSize ().Width + : scroll.SuperView?.GetContentSize ().Height))) + { + // TODO: This must be handled in the ScrollSlider if Width and Height being virtual + e.Cancel = true; - return; - } + return; + } - if (scroll.Orientation == Orientation.Vertical) - { - scroll.Width = e.NewValue; - } - else - { - scroll.Height = e.NewValue; - } - }; + if (scroll.Orientation == Orientation.Vertical) + { + scroll.Width = e.NewValue; + } + else + { + scroll.Height = e.NewValue; + } + }; + + + var lblOrientationabel = new Label + { + Text = "_Orientation:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) + }; + frameView.Add (lblOrientationabel); var rgOrientation = new RadioGroup { - Y = Pos.Bottom (lblWidthHeight), + X = Pos.Right (lblOrientationabel) + 1, + Y = Pos.Top (lblOrientationabel), RadioLabels = ["Vertical", "Horizontal"], Orientation = Orientation.Horizontal }; frameView.Add (rgOrientation); rgOrientation.SelectedItemChanged += (s, e) => - { - if (e.SelectedItem == e.PreviousSelectedItem) - { - return; - } + { + if (e.SelectedItem == e.PreviousSelectedItem) + { + return; + } - if (rgOrientation.SelectedItem == 0) - { - scroll.Orientation = Orientation.Vertical; - scroll.X = Pos.AnchorEnd (); - scroll.Y = 0; - scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Width); - scroll.Width = scrollWidthHeight.Value; - scroll.Height = Dim.Fill (); - scroll.Size /= 3; - } - else - { - scroll.Orientation = Orientation.Horizontal; - scroll.X = 0; - scroll.Y = Pos.AnchorEnd (); - scroll.Width = Dim.Fill (); - - scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Height); - scroll.Height = scrollWidthHeight.Value; - scroll.Size *= 3; - } - }; + if (rgOrientation.SelectedItem == 0) + { + scroll.Orientation = Orientation.Vertical; + scroll.X = Pos.AnchorEnd (); + scroll.Y = 0; + scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Width); + scroll.Width = scrollWidthHeight.Value; + } + else + { + scroll.Orientation = Orientation.Horizontal; + scroll.X = 0; + scroll.Y = Pos.AnchorEnd (); + scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Height); + scroll.Height = scrollWidthHeight.Value; + } + }; var lblSize = new Label { - Y = Pos.Bottom (rgOrientation), - Text = "Size:" + Text = "_Size:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) }; frameView.Add (lblSize); @@ -133,106 +155,109 @@ public class ScrollDemo : Scenario X = Pos.Right (lblSize) + 1, Y = Pos.Top (lblSize) }; - scroll.SizeChanged += (sender, args) => scrollSize.Value = args.CurrentValue; frameView.Add (scrollSize); scrollSize.ValueChanging += (s, e) => - { - if (e.NewValue < 0) - { - e.Cancel = true; - - return; - } - - if (scroll.Size != e.NewValue) - { - scroll.Size = e.NewValue; - } - }; - - var lblPosition = new Label { - Y = Pos.Bottom (lblSize), - Text = "Position:" - }; - frameView.Add (lblPosition); + if (e.NewValue < 0) + { + e.Cancel = true; - NumericUpDown scrollPosition = new () + return; + } + + if (scroll.Size != e.NewValue) + { + scroll.Size = e.NewValue; + } + }; + + var lblSliderPosition = new Label { - Value = scroll.SliderPosition, - X = Pos.Right (lblPosition) + 1, - Y = Pos.Top (lblPosition) + Text = "_SliderPosition:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) + }; - frameView.Add (scrollPosition); + frameView.Add (lblSliderPosition); - scrollPosition.ValueChanging += (s, e) => - { - if (e.NewValue < 0) - { - e.Cancel = true; + Label scrollSliderPosition = new () + { + Text = scroll.GetSliderPosition ().ToString (), + X = Pos.Right (lblSliderPosition) + 1, + Y = Pos.Top (lblSliderPosition) + }; + frameView.Add (scrollSliderPosition); - return; - } + var lblContentPosition = new Label + { + Text = "_ContentPosition:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) - if (scroll.SliderPosition != e.NewValue) - { - scroll.SliderPosition = e.NewValue; - } + }; + frameView.Add (lblContentPosition); - if (scroll.SliderPosition != e.NewValue) - { - e.Cancel = true; - } - }; + NumericUpDown scrollContentPosition = new () + { + Value = scroll.GetSliderPosition (), + X = Pos.Right (lblContentPosition) + 1, + Y = Pos.Top (lblContentPosition) + }; + frameView.Add (scrollContentPosition); + + scrollContentPosition.ValueChanging += (s, e) => + { + if (e.NewValue < 0) + { + e.Cancel = true; + + return; + } + + if (scroll.ContentPosition != e.NewValue) + { + scroll.ContentPosition = e.NewValue; + } + + if (scroll.ContentPosition != e.NewValue) + { + e.Cancel = true; + } + }; + + var lblOptions = new Label + { + Text = "_Options:", + TextAlignment = Alignment.End, + Y = Pos.Align (Alignment.Start, groupId: 1), + Width = Dim.Func (() => GetMaxLabelWidth (1)) + }; + frameView.Add (lblOptions); + + var ckbShowPercent = new CheckBox + { + Y = Pos.Top (lblOptions), + X = Pos.Right (lblOptions) + 1, + Text = "Sho_wPercent", + CheckedState = scroll.ShowPercent ? CheckState.Checked : CheckState.UnChecked + }; + ckbShowPercent.CheckedStateChanging += (s, e) => scroll.ShowPercent = e.NewValue == CheckState.Checked; + frameView.Add (ckbShowPercent); //var ckbKeepContentInAllViewport = new CheckBox //{ - // Y = Pos.Bottom (scrollPosition), Text = "KeepContentInAllViewport", - // CheckedState = scroll.KeepContentInAllViewport ? CheckState.Checked : CheckState.UnChecked + // X = Pos.Right (ckbShowScrollIndicator) + 1, Y = Pos.Bottom (scrollPosition), Text = "KeepContentInAllViewport", + // CheckedState = Scroll.KeepContentInAllViewport ? CheckState.Checked : CheckState.UnChecked //}; - //ckbKeepContentInAllViewport.CheckedStateChanging += (s, e) => scroll.KeepContentInAllViewport = e.NewValue == CheckState.Checked; + //ckbKeepContentInAllViewport.CheckedStateChanging += (s, e) => Scroll.KeepContentInAllViewport = e.NewValue == CheckState.Checked; //view.Add (ckbKeepContentInAllViewport); - var lblSizeChanged = new Label - { - Y = Pos.Bottom (scrollPosition) + 1 - }; - frameView.Add (lblSizeChanged); - - scroll.SizeChanged += (s, e) => - { - lblSizeChanged.Text = $"SizeChanged event - CurrentValue: {e.CurrentValue}"; - - if (scrollSize.Value != e.CurrentValue) - { - scrollSize.Value = e.CurrentValue; - } - }; - - var lblPosChanging = new Label - { - Y = Pos.Bottom (lblSizeChanged) - }; - frameView.Add (lblPosChanging); - - scroll.SliderPositionChanging += (s, e) => { lblPosChanging.Text = $"PositionChanging event - CurrentValue: {e.CurrentValue}; NewValue: {e.NewValue}"; }; - - var lblPositionChanged = new Label - { - Y = Pos.Bottom (lblPosChanging) - }; - frameView.Add (lblPositionChanged); - - scroll.SliderPositionChanged += (s, e) => - { - lblPositionChanged.Text = $"PositionChanged event - CurrentValue: {e.CurrentValue}"; - scrollPosition.Value = e.CurrentValue; - }; - var lblScrollFrame = new Label { - Y = Pos.Bottom (lblPositionChanged) + 1 + Y = Pos.Bottom (lblOptions) + 1 }; frameView.Add (lblScrollFrame); @@ -248,21 +273,59 @@ public class ScrollDemo : Scenario }; frameView.Add (lblScrollContentSize); - scroll.SubviewsLaidOut += (s, e) => - { - lblScrollFrame.Text = $"Scroll Frame: {scroll.Frame.ToString ()}"; - lblScrollViewport.Text = $"Scroll Viewport: {scroll.Viewport.ToString ()}"; - lblScrollContentSize.Text = $"Scroll ContentSize: {scroll.GetContentSize ().ToString ()}"; - }; + { + lblScrollFrame.Text = $"Scroll Frame: {scroll.Frame.ToString ()}"; + lblScrollViewport.Text = $"Scroll Viewport: {scroll.Viewport.ToString ()}"; + lblScrollContentSize.Text = $"Scroll ContentSize: {scroll.GetContentSize ().ToString ()}"; + }; - editor.Initialized += (s, e) => - { - scroll.Size = int.Max (app.GetContentSize ().Height * 2, app.GetContentSize ().Width * 2); - editor.ViewToEdit = scroll; - }; + EventLog eventLog = new () + { + X = Pos.AnchorEnd () - 1, + Y = 0, + Height = Dim.Height (frameView), + BorderStyle = LineStyle.Single, + ViewToLog = scroll + }; + app.Add (eventLog); + frameView.Width = Dim.Fill (Dim.Func (() => Math.Max (28, eventLog.Frame.Width + 1))); - app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags; + app.Initialized += AppOnInitialized; + + + void AppOnInitialized (object sender, EventArgs e) + { + scroll.SizeChanged += (s, e) => + { + eventLog.Log ($"SizeChanged: {e.CurrentValue}"); + + if (scrollSize.Value != e.CurrentValue) + { + scrollSize.Value = e.CurrentValue; + } + }; + + scroll.SliderPositionChanged += (s, e) => + { + eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}"); + eventLog.Log ($" ContentPosition: {scroll.ContentPosition}"); + scrollSliderPosition.Text = e.CurrentValue.ToString (); + }; + + scroll.ContentPositionChanged += (s, e) => + { + eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}"); + eventLog.Log ($" ContentPosition: {scroll.ContentPosition}"); + scrollContentPosition.Value = e.CurrentValue; + }; + + editor.Initialized += (s, e) => + { + editor.ViewToEdit = scroll; + }; + + } Application.Run (app); app.Dispose (); diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index e76900bca..e1407506a 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -65,8 +65,8 @@ public class TableEditor : Scenario "Cuneiform Numbers and Punctuation" ), new ( - (uint)(CharMap._maxCodePoint - 16), - (uint)CharMap._maxCodePoint, + (uint)(UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End) - 16), + (uint)UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End), "End" ), new (0x0020, 0x007F, "Basic Latin"), @@ -1533,17 +1533,10 @@ public class TableEditor : Scenario ); } - private class UnicodeRange + public class UnicodeRange (uint start, uint end, string category) { - public readonly string Category; - public readonly uint End; - public readonly uint Start; - - public UnicodeRange (uint start, uint end, string category) - { - Start = start; - End = end; - Category = category; - } + public readonly string Category = category; + public readonly uint End = end; + public readonly uint Start = start; } } diff --git a/UnitTests/Views/ScrollBarTests.cs b/UnitTests/Views/ScrollBarTests.cs index 460ea18bf..a0e177fce 100644 --- a/UnitTests/Views/ScrollBarTests.cs +++ b/UnitTests/Views/ScrollBarTests.cs @@ -52,7 +52,7 @@ public class ScrollBarTests Assert.False (scrollBar.CanFocus); Assert.Equal (Orientation.Vertical, scrollBar.Orientation); Assert.Equal (0, scrollBar.Size); - Assert.Equal (0, scrollBar.SliderPosition); + Assert.Equal (0, scrollBar.GetSliderPosition ()); Assert.True (scrollBar.AutoHide); } @@ -82,43 +82,10 @@ public class ScrollBarTests }; super.Add (scrollBar); scrollBar.Layout (); - scrollBar.SliderPosition = 1; + scrollBar.ContentPosition = 1; scrollBar.Orientation = Orientation.Horizontal; - Assert.Equal (0, scrollBar.SliderPosition); - } - - - [Fact] - public void SliderPosition_Event_Cancelables () - { - var changingCount = 0; - var changedCount = 0; - var scrollBar = new ScrollBar { }; - scrollBar.Layout (); - scrollBar.Size = scrollBar.Viewport.Height * 2; - scrollBar.Layout (); - - scrollBar.SliderPositionChanging += (s, e) => - { - if (changingCount == 0) - { - e.Cancel = true; - } - - changingCount++; - }; - scrollBar.SliderPositionChanged += (s, e) => changedCount++; - - scrollBar.SliderPosition = 1; - Assert.Equal (0, scrollBar.SliderPosition); - Assert.Equal (1, changingCount); - Assert.Equal (0, changedCount); - - scrollBar.SliderPosition = 1; - Assert.Equal (1, scrollBar.SliderPosition); - Assert.Equal (2, changingCount); - Assert.Equal (1, changedCount); + Assert.Equal (0, scrollBar.GetSliderPosition ()); } @@ -179,10 +146,10 @@ public class ScrollBarTests Assert.Equal (30, scrollBar.Size); scrollBar.KeepContentInAllViewport = false; - scrollBar.SliderPosition = 50; - Assert.Equal (scrollBar.SliderPosition, scrollBar.Size - 1); - Assert.Equal (scrollBar.SliderPosition, view.Viewport.Y); - Assert.Equal (29, scrollBar.SliderPosition); + scrollBar.ContentPosition = 50; + Assert.Equal (scrollBar.GetSliderPosition (), scrollBar.Size - 1); + Assert.Equal (scrollBar.GetSliderPosition (), view.Viewport.Y); + Assert.Equal (29, scrollBar.GetSliderPosition ()); Assert.Equal (29, view.Viewport.Y); top.Dispose (); @@ -198,7 +165,7 @@ public class ScrollBarTests var scrollBar = new ScrollBar { X = 10, Y = 10, Width = orientation == Orientation.Vertical ? 1 : 10, Height = orientation == Orientation.Vertical ? 10 : 1, Size = 20, - SliderPosition = 5, Orientation = orientation, KeepContentInAllViewport = true + ContentPosition = 5, Orientation = orientation, KeepContentInAllViewport = true }; var top = new Toplevel (); top.Add (scrollBar); @@ -346,7 +313,7 @@ public class ScrollBarTests scrollBar.Size = sliderSize; scrollBar.Layout (); - scrollBar.SliderPosition = sliderPosition; + scrollBar.ContentPosition = sliderPosition; super.BeginInit (); super.EndInit (); @@ -392,10 +359,10 @@ public class ScrollBarTests top.Add (scrollBar); RunState rs = Application.Begin (top); - scrollBar.SliderPosition = 5; + scrollBar.ContentPosition = 5; Application.RunIteration (ref rs); - Assert.Equal (5, scrollBar.SliderPosition); + Assert.Equal (5, scrollBar.GetSliderPosition ()); Assert.Equal (12, scrollBar.ContentPosition); int initialPos = scrollBar.ContentPosition; @@ -433,10 +400,10 @@ public class ScrollBarTests top.Add (scrollBar); RunState rs = Application.Begin (top); - scrollBar.SliderPosition = 0; + scrollBar.ContentPosition = 0; Application.RunIteration (ref rs); - Assert.Equal (0, scrollBar.SliderPosition); + Assert.Equal (0, scrollBar.GetSliderPosition ()); Assert.Equal (0, scrollBar.ContentPosition); int initialPos = scrollBar.ContentPosition; diff --git a/UnitTests/Views/ScrollTests.cs b/UnitTests/Views/ScrollTests.cs index 7afb6afc8..00b7481e7 100644 --- a/UnitTests/Views/ScrollTests.cs +++ b/UnitTests/Views/ScrollTests.cs @@ -21,7 +21,7 @@ public class ScrollTests } [Fact] - public void OnOrientationChanged_Sets_Position_To_0 () + public void OnOrientationChanged_Sets_ContentPosition_To_0 () { View super = new View () { @@ -34,10 +34,10 @@ public class ScrollTests }; super.Add (scroll); scroll.Layout (); - scroll.SliderPosition = 1; + scroll.ContentPosition = 1; scroll.Orientation = Orientation.Horizontal; - Assert.Equal (0, scroll.SliderPosition); + Assert.Equal (0, scroll.ContentPosition); } @@ -224,7 +224,7 @@ public class ScrollTests Assert.False (scroll.CanFocus); Assert.Equal (Orientation.Vertical, scroll.Orientation); Assert.Equal (0, scroll.Size); - Assert.Equal (0, scroll.SliderPosition); + Assert.Equal (0, scroll.GetSliderPosition ()); } //[Fact] @@ -324,7 +324,7 @@ public class ScrollTests top.Add (scroll); RunState rs = Application.Begin (top); - Assert.Equal (0, scroll.SliderPosition); + Assert.Equal (0, scroll.GetSliderPosition ()); Assert.Equal (0, scroll.ContentPosition); Application.RaiseMouseEvent (new () @@ -377,10 +377,10 @@ public class ScrollTests top.Add (scroll); RunState rs = Application.Begin (top); - scroll.SliderPosition = 5; + scroll.ContentPosition = 5; Application.RunIteration (ref rs); - Assert.Equal (5, scroll.SliderPosition); + Assert.Equal (5, scroll.GetSliderPosition ()); Assert.Equal (10, scroll.ContentPosition); Application.RaiseMouseEvent (new () @@ -390,7 +390,7 @@ public class ScrollTests }); Application.RunIteration (ref rs); - Assert.Equal (0, scroll.SliderPosition); + Assert.Equal (0, scroll.GetSliderPosition ()); Assert.Equal (0, scroll.ContentPosition); Application.ResetState (true); @@ -421,42 +421,10 @@ public class ScrollTests scroll.Size = scrollSize; super.Layout (); - scroll.SliderPosition = scrollPosition; + scroll.ContentPosition = scrollPosition; super.Layout (); - Assert.True (scroll.SliderPosition <= scrollSize); - } - - [Fact] - public void SliderPosition_Event_Cancelables () - { - var changingCount = 0; - var changedCount = 0; - var scroll = new Scroll { }; - scroll.Layout (); - scroll.Size = scroll.Viewport.Height * 2; - scroll.Layout (); - - scroll.SliderPositionChanging += (s, e) => - { - if (changingCount == 0) - { - e.Cancel = true; - } - - changingCount++; - }; - scroll.SliderPositionChanged += (s, e) => changedCount++; - - scroll.SliderPosition = 1; - Assert.Equal (0, scroll.SliderPosition); - Assert.Equal (1, changingCount); - Assert.Equal (0, changedCount); - - scroll.SliderPosition = 1; - Assert.Equal (1, scroll.SliderPosition); - Assert.Equal (2, changingCount); - Assert.Equal (1, changedCount); + Assert.True (scroll.GetSliderPosition () <= scrollSize); } [Fact] @@ -466,60 +434,52 @@ public class ScrollTests var cancel = false; var changed = 0; var scroll = new Scroll { Height = 10, Size = 20 }; - scroll.SliderPositionChanging += Scroll_PositionChanging; scroll.SliderPositionChanged += Scroll_PositionChanged; Assert.Equal (Orientation.Vertical, scroll.Orientation); scroll.Layout (); Assert.Equal (new (0, 0, 1, 10), scroll.Viewport); - Assert.Equal (0, scroll.SliderPosition); + Assert.Equal (0, scroll.GetSliderPosition ()); Assert.Equal (0, changing); Assert.Equal (0, changed); - scroll.SliderPosition = 0; - Assert.Equal (0, scroll.SliderPosition); + scroll.ContentPosition = 0; + Assert.Equal (0, scroll.GetSliderPosition ()); Assert.Equal (0, changing); Assert.Equal (0, changed); - scroll.SliderPosition = 1; - Assert.Equal (1, scroll.SliderPosition); + scroll.ContentPosition = 1; + Assert.Equal (1, scroll.GetSliderPosition ()); Assert.Equal (1, changing); Assert.Equal (1, changed); Reset (); cancel = true; - scroll.SliderPosition = 2; - Assert.Equal (1, scroll.SliderPosition); + scroll.ContentPosition = 2; + Assert.Equal (1, scroll.GetSliderPosition ()); Assert.Equal (1, changing); Assert.Equal (0, changed); Reset (); - scroll.SliderPosition = 10; - Assert.Equal (5, scroll.SliderPosition); + scroll.ContentPosition = 10; + Assert.Equal (5, scroll.GetSliderPosition ()); Assert.Equal (1, changing); Assert.Equal (1, changed); Reset (); - scroll.SliderPosition = 11; - Assert.Equal (5, scroll.SliderPosition); + scroll.ContentPosition = 11; + Assert.Equal (5, scroll.GetSliderPosition ()); Assert.Equal (1, changing); Assert.Equal (1, changed); Reset (); - scroll.SliderPosition = 0; - Assert.Equal (0, scroll.SliderPosition); + scroll.ContentPosition = 0; + Assert.Equal (0, scroll.GetSliderPosition ()); Assert.Equal (1, changing); Assert.Equal (1, changed); - scroll.SliderPositionChanging -= Scroll_PositionChanging; scroll.SliderPositionChanged -= Scroll_PositionChanged; - void Scroll_PositionChanging (object sender, CancelEventArgs e) - { - changing++; - e.Cancel = cancel; - } - void Scroll_PositionChanged (object sender, EventArgs e) { changed++; } void Reset () @@ -625,7 +585,7 @@ public class ScrollTests scroll.Size = sliderSize; scroll.Layout (); - scroll.SliderPosition = sliderPosition; + scroll.ContentPosition = sliderPosition; super.BeginInit (); super.EndInit ();