Refactored and went back and forth. Things are working well. Tests not so much

This commit is contained in:
Tig
2024-11-11 00:50:32 -05:00
parent 6f9816bbb8
commit f7e0a293c5
14 changed files with 1176 additions and 1018 deletions

View File

@@ -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;
}
};

View File

@@ -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);
}
/// <inheritdoc/>
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;
/// <summary>
/// Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
/// </summary>
/// <remarks>
/// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
/// </remarks>
public int ViewportDimension
{
get
{
if (_viewportDimension.HasValue)
{
return _viewportDimension.Value;
}
return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
}
set => _viewportDimension = value;
}
private int _size;
/// <summary>
/// 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.
/// </summary>
public int Size
{
@@ -135,43 +157,37 @@ public class Scroll : View, IOrientation, IDesignable
public event EventHandler<EventArgs<int>>? SizeChanged;
#region SliderPosition
private void OnSliderOnFrameChanged (object? sender, EventArgs<Rectangle> args)
private void SliderOnPositionChanged (object? sender, EventArgs<int> 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<int> e)
{
if (ViewportDimension == 0)
{
return;
}
}
/// <summary>
/// Gets or sets the position of the start of the Scroll slider, within the Viewport.
/// </summary>
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<int> 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));
}
/// <summary>
/// Called when <see cref="SliderPosition"/> is changing. Return true to cancel the change.
/// </summary>
protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; }
/// <summary>
/// Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
/// <see langword="true"/> to prevent the position from being changed.
/// </summary>
public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
/// <summary>Called when <see cref="SliderPosition"/> has changed.</summary>
/// <summary>Called when the slider position has changed.</summary>
protected virtual void OnSliderPositionChanged (int position) { }
/// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
/// <summary>Raised when the slider position has changed.</summary>
public event EventHandler<EventArgs<int>>? SliderPositionChanged;
private int CalculateSliderPosition (int contentPosition)
@@ -219,8 +222,17 @@ public class Scroll : View, IOrientation, IDesignable
private int _contentPosition;
/// <summary>
/// Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
/// Gets or sets the position of the slider relative to <see cref="Size"/>.
/// </summary>
/// <remarks>
/// <para>
/// The content position is clamped to 0 and <see cref="Size"/> minus <see cref="ViewportDimension"/>.
/// </para>
/// <para>
/// Setting will result in the <see cref="ContentPositionChanging"/> and <see cref="ContentPositionChanged"/>
/// events being raised.
/// </para>
/// </remarks>
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
/// <inheritdoc/>
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;
}
/// <summary>
/// Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
/// </summary>

View File

@@ -11,9 +11,6 @@ namespace Terminal.Gui;
/// and clicking with the mouse to scroll.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="SliderPosition"/> indicates the number of rows or columns the Scroll has moved from 0.
/// </para>
/// </remarks>
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
}
/// <summary>Gets or sets the position of the slider within the ScrollBar's Viewport.</summary>
/// <value>The position.</value>
public int SliderPosition
{
get => _scroll.SliderPosition;
set => _scroll.SliderPosition = value;
}
/// <returns>The position.</returns>
public int GetSliderPosition () => _scroll.GetSliderPosition ();
private void OnScrollOnSliderPositionChanging (object? sender, CancelEventArgs<int> e) { SliderPositionChanging?.Invoke (this, e); }
private void OnScrollOnSliderPositionChanged (object? sender, EventArgs<int> e) { SliderPositionChanged?.Invoke (this, e); }
/// <summary>
/// Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
/// <see langword="true"/> to prevent the position from being changed.
/// </summary>
public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
/// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
/// <summary>Raised when the position of the slider has changed.</summary>
public event EventHandler<EventArgs<int>>? SliderPositionChanged;
/// <summary>
/// Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through.
/// </summary>
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;
}
/// <summary>
/// Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
/// </summary>
/// <remarks>
/// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
/// </remarks>
public int ViewportDimension
{
get => _scroll.ViewportDimension;
set => _scroll.ViewportDimension = value;
}
/// <summary>
/// Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
/// </summary>
@@ -233,12 +222,12 @@ public class ScrollBar : View, IOrientation, IDesignable
private void OnScrollOnContentPositionChanged (object? sender, EventArgs<int> e) { ContentPositionChanged?.Invoke (this, e); }
/// <summary>
/// Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
/// Raised when the <see cref="ContentPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
/// <see langword="true"/> to prevent the position from being changed.
/// </summary>
public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
/// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
/// <summary>Raised when the <see cref="ContentPosition"/> has changed.</summary>
public event EventHandler<EventArgs<int>>? ContentPositionChanged;
/// <summary>Raised when <see cref="Size"/> has changed.</summary>
@@ -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;
}

View File

@@ -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
}
/// <summary>
/// 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
/// <see cref="Orientation"/>. 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 <see cref="Size"/>.
/// </summary>
/// <remarks>
/// This is the SuperView's Viewport demension.
/// </remarks>
public int ViewportDimension => Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 0 : SuperView?.Viewport.Width ?? 0;
private void OnFrameChanged (object? sender, EventArgs<Rectangle> e)
{
Position = Orientation == Orientation.Vertical ? e.CurrentValue.Y : e.CurrentValue.X;
}
private int _position;
/// <summary>
/// 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 <see cref="View.SuperView"/>.
/// </summary>
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<int> 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));
}
/// <summary>
/// Called when <see cref="Position"/> is changing. Return true to cancel the change.
/// </summary>
protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
/// <summary>
/// Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
/// <see langword="true"/> to prevent the position from being changed.
/// </summary>
public event EventHandler<CancelEventArgs<int>>? PositionChanging;
/// <summary>Called when <see cref="Position"/> has changed.</summary>
protected virtual void OnPositionChanged (int position) { }
/// <summary>Raised when the <see cref="Position"/> has changed.</summary>
public event EventHandler<EventArgs<int>>? PositionChanged;
/// <summary>Called when <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
protected virtual void OnScroll (int scrollAmount) { }
/// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
public event EventHandler<EventArgs<int>>? Scroll;
/// <inheritdoc/>
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;

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[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
/// <remarks>
/// See <see href="CharacterMap/README.md"/> for details.
/// </remarks>
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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
isDescending = !isDescending;
_categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
{
Func<UnicodeRange, object> 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<UnicodeRange> 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<Key> GetDemoKeyStrokes ()
{
List<Key> 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;
/// <summary>
/// Initalizes a new instance.
/// </summary>
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
/// <summary>Gets or sets the coordinates of the Cursor based on the SelectedCodePoint in Viewport-relative coordinates</summary>
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
/// <summary>
/// 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
/// </summary>
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)
/// <summary>
/// Raised when the selected code point changes.
/// </summary>
public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
/// <summary>
/// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
/// characters.
/// </summary>
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;
}
/// <summary>
/// Gets or sets whether the number of columns each glyph is displayed.
/// </summary>
public bool ShowGlyphWidths
{
get => _rowHeight == 2;
@@ -636,25 +338,12 @@ internal class CharMap : View, IDesignable
}
}
/// <summary>
/// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
/// characters.
/// </summary>
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<ListViewItemEventArgs> 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<ListViewItemEventArgs> SelectedCodePointChanged;
/// <summary>
/// Helper to convert a string into camel case.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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<ListViewItemEventArgs>? 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<string> GetChars (string chars)
{
HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> GetCharsName (string chars)
{
HttpResponseMessage response =
await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> GetCodepointDec (int dec)
{
HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> 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<UnicodeRange> 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<UnicodeRange> GetRanges ()
{
IEnumerable<UnicodeRange> 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<UnicodeRange> 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
}

View File

@@ -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;
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// See <see href="CharacterMap/README.md"/>.
/// </remarks>
[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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
isDescending = !isDescending;
_categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_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<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
{
Func<UnicodeRange, object> 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<UnicodeRange> 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<Key> GetDemoKeyStrokes ()
{
List<Key> 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;
}
}

View File

@@ -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** -

View File

@@ -0,0 +1,48 @@
#nullable enable
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace UICatalog.Scenarios;
/// <summary>
/// A helper class for accessing the ucdapi.org API.
/// </summary>
public class UcdApiClient
{
public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
private static readonly HttpClient _httpClient = new ();
public async Task<string> GetChars (string chars)
{
HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> GetCharsName (string chars)
{
HttpResponseMessage response =
await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> GetCodepointDec (int dec)
{
HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
public async Task<string> GetCodepointHex (string hex)
{
HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
response.EnsureSuccessStatusCode ();
return await response.Content.ReadAsStringAsync ();
}
}

View File

@@ -0,0 +1,101 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Unicode;
namespace UICatalog.Scenarios;
/// <summary>
/// Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus
/// the non-BMP ranges not included.
/// </summary>
public class UnicodeRange (int start, int end, string category)
{
/// <summary>
/// Gets the list of all ranges.
/// </summary>
public static List<UnicodeRange> Ranges => GetRanges ();
/// <summary>
/// The category.
/// </summary>
public string Category { get; set; } = category;
/// <summary>
/// Te codepoint at the start of the range.
/// </summary>
public int Start { get; set; } = start;
/// <summary>
/// The codepoint at the end of the range.
/// </summary>
public int End { get; set; } = end;
/// <summary>
/// Gets the list of all ranges..
/// </summary>
/// <returns></returns>
public static List<UnicodeRange> GetRanges ()
{
IEnumerable<UnicodeRange> 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<UnicodeRange> 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 ();
}
}

View File

@@ -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<int> 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<int> 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;
};

View File

@@ -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<PosAlign> (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<int> 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<int> 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 ();

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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<int> e)
{
changing++;
e.Cancel = cancel;
}
void Scroll_PositionChanged (object sender, EventArgs<int> 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 ();