mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Merge branch 'v2_develop' into copilot/fix-3ee850b1-eb6b-46b0-964b-3b98d2c0c14e
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"Themes": [
|
||||
{
|
||||
"Hot Dog Stand": {
|
||||
"Glyphs.WideGlyphReplacement": "①",
|
||||
"Schemes": [
|
||||
{
|
||||
"Runnable": {
|
||||
@@ -134,7 +135,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"UI Catalog Theme": {
|
||||
"UI Catalog": {
|
||||
"Window.DefaultShadow": "Transparent",
|
||||
"Button.DefaultShadow": "None",
|
||||
"CheckBox.DefaultHighlightStates": "In, Pressed, PressedOutside",
|
||||
|
||||
@@ -19,6 +19,7 @@ public class Adornments : Scenario
|
||||
|
||||
var editor = new AdornmentsEditor
|
||||
{
|
||||
BorderStyle = LineStyle.Single,
|
||||
AutoSelectViewToEdit = true,
|
||||
|
||||
// This is for giggles, to show that the editor can be moved around.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,11 +55,13 @@ public class AdornmentEditor : EditorBase
|
||||
_bottomEdit!.Value = _adornment.Thickness.Bottom;
|
||||
_rightEdit!.Value = _adornment.Thickness.Right;
|
||||
|
||||
_adornment.Initialized += (sender, args) =>
|
||||
_adornment.Initialized += (_, _) =>
|
||||
{
|
||||
Scheme? cs = _adornment.GetScheme ();
|
||||
_foregroundColorPicker.SelectedColor = _adornment.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ();
|
||||
_backgroundColorPicker.SelectedColor = _adornment.GetAttributeForRole (VisualRole.Normal).Background.GetClosestNamedColor16 ();
|
||||
_foregroundColorPicker.SelectedColor =
|
||||
_adornment.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ();
|
||||
|
||||
_backgroundColorPicker.SelectedColor =
|
||||
_adornment.GetAttributeForRole (VisualRole.Normal).Background.GetClosestNamedColor16 ();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,12 +125,12 @@ public class AdornmentEditor : EditorBase
|
||||
_bottomEdit.ValueChanging += Bottom_ValueChanging;
|
||||
Add (_bottomEdit);
|
||||
|
||||
var copyTop = new Button
|
||||
Button copyTop = new ()
|
||||
{
|
||||
X = Pos.Center (), Y = Pos.Bottom (_bottomEdit), Text = "Cop_y Top"
|
||||
};
|
||||
|
||||
copyTop.Accepting += (s, e) =>
|
||||
copyTop.Accepting += (_, _) =>
|
||||
{
|
||||
AdornmentToEdit!.Thickness = new (_topEdit.Value);
|
||||
_leftEdit.Value = _rightEdit.Value = _bottomEdit.Value = _topEdit.Value;
|
||||
@@ -168,9 +168,9 @@ public class AdornmentEditor : EditorBase
|
||||
_diagThicknessCheckBox.CheckedState = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Thickness) ? CheckState.Checked : CheckState.UnChecked;
|
||||
}
|
||||
|
||||
_diagThicknessCheckBox.CheckedStateChanging += (s, e) =>
|
||||
_diagThicknessCheckBox.CheckedStateChanging += (_, args) =>
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (args.Result == CheckState.Checked)
|
||||
{
|
||||
AdornmentToEdit!.Diagnostics |= ViewDiagnosticFlags.Thickness;
|
||||
}
|
||||
@@ -194,9 +194,9 @@ public class AdornmentEditor : EditorBase
|
||||
_diagRulerCheckBox.CheckedState = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Ruler) ? CheckState.Checked : CheckState.UnChecked;
|
||||
}
|
||||
|
||||
_diagRulerCheckBox.CheckedStateChanging += (s, e) =>
|
||||
_diagRulerCheckBox.CheckedStateChanging += (_, args) =>
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (args.Result == CheckState.Checked)
|
||||
{
|
||||
AdornmentToEdit!.Diagnostics |= ViewDiagnosticFlags.Ruler;
|
||||
}
|
||||
@@ -212,18 +212,19 @@ public class AdornmentEditor : EditorBase
|
||||
|
||||
private EventHandler<ResultEventArgs<Color>> ColorPickerColorChanged ()
|
||||
{
|
||||
return (o, a) =>
|
||||
return (_, _) =>
|
||||
{
|
||||
if (AdornmentToEdit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AdornmentToEdit.SetScheme (new (AdornmentToEdit.GetScheme ())
|
||||
{
|
||||
Normal = new (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor)
|
||||
})
|
||||
;
|
||||
AdornmentToEdit.SetScheme (
|
||||
new (AdornmentToEdit.GetScheme ())
|
||||
{
|
||||
Normal = new (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor)
|
||||
})
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ public class AdornmentsEditor : EditorBase
|
||||
|
||||
TabStop = TabBehavior.TabGroup;
|
||||
|
||||
ExpanderButton!.Orientation = Orientation.Horizontal;
|
||||
|
||||
Initialized += AdornmentsEditor_Initialized;
|
||||
|
||||
SchemeName = "Dialog";
|
||||
@@ -28,8 +26,6 @@ public class AdornmentsEditor : EditorBase
|
||||
/// <inheritdoc/>
|
||||
protected override void OnViewToEditChanged ()
|
||||
{
|
||||
//Enabled = ViewToEdit is not Adornment;
|
||||
|
||||
if (MarginEditor is { })
|
||||
{
|
||||
MarginEditor.AdornmentToEdit = ViewToEdit?.Margin ?? null;
|
||||
@@ -47,7 +43,7 @@ public class AdornmentsEditor : EditorBase
|
||||
|
||||
if (Padding is { })
|
||||
{
|
||||
Padding.Text = $"View: {GetIdentifyingString (ViewToEdit)}";
|
||||
Padding.Text = GetIdentifyingString (ViewToEdit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +88,17 @@ public class AdornmentsEditor : EditorBase
|
||||
|
||||
private void AdornmentsEditor_Initialized (object? sender, EventArgs e)
|
||||
{
|
||||
if (ExpanderButton is { })
|
||||
{
|
||||
ExpanderButton.Orientation = Orientation.Horizontal;
|
||||
}
|
||||
|
||||
MarginEditor = new ()
|
||||
{
|
||||
X = -1,
|
||||
Y = 0,
|
||||
SuperViewRendersLineCanvas = true,
|
||||
BorderStyle = LineStyle.Single
|
||||
BorderStyle = BorderStyle
|
||||
};
|
||||
MarginEditor.Border!.Thickness = MarginEditor.Border!.Thickness with { Bottom = 0 };
|
||||
Add (MarginEditor);
|
||||
@@ -107,7 +108,7 @@ public class AdornmentsEditor : EditorBase
|
||||
X = Pos.Left (MarginEditor),
|
||||
Y = Pos.Bottom (MarginEditor),
|
||||
SuperViewRendersLineCanvas = true,
|
||||
BorderStyle = LineStyle.Single
|
||||
BorderStyle = BorderStyle
|
||||
};
|
||||
BorderEditor.Border!.Thickness = BorderEditor.Border!.Thickness with { Bottom = 0 };
|
||||
Add (BorderEditor);
|
||||
@@ -117,7 +118,7 @@ public class AdornmentsEditor : EditorBase
|
||||
X = Pos.Left (BorderEditor),
|
||||
Y = Pos.Bottom (BorderEditor),
|
||||
SuperViewRendersLineCanvas = true,
|
||||
BorderStyle = LineStyle.Single
|
||||
BorderStyle = BorderStyle
|
||||
};
|
||||
PaddingEditor.Border!.Thickness = PaddingEditor.Border!.Thickness with { Bottom = 0 };
|
||||
Add (PaddingEditor);
|
||||
|
||||
@@ -77,7 +77,7 @@ public class AllViewsView : View
|
||||
|
||||
View? previousView = null;
|
||||
|
||||
foreach (Type? type in allClasses)
|
||||
foreach (Type type in allClasses)
|
||||
{
|
||||
View? view = CreateView (type);
|
||||
|
||||
@@ -118,15 +118,8 @@ public class AllViewsView : View
|
||||
// Check if the generic parameter has constraints
|
||||
Type [] constraints = arg.GetGenericParameterConstraints ();
|
||||
|
||||
if (constraints.Length > 0)
|
||||
{
|
||||
// Use the first constraint type to satisfy the constraint
|
||||
typeArguments.Add (constraints [0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
typeArguments.Add (typeof (object));
|
||||
}
|
||||
// Use the first constraint type to satisfy the constraint
|
||||
typeArguments.Add (constraints.Length > 0 ? constraints [0] : typeof (object));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,17 +186,17 @@ public class AllViewsView : View
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.Width == Dim.Absolute (0) || view.Width is null)
|
||||
if (view.Width == Dim.Absolute (0))
|
||||
{
|
||||
view.Width = Dim.Fill ();
|
||||
}
|
||||
|
||||
if (view.Height == Dim.Absolute (0) || view.Height is null)
|
||||
if (view.Height == Dim.Absolute (0))
|
||||
{
|
||||
view.Height = MAX_VIEW_FRAME_HEIGHT - 2;
|
||||
}
|
||||
|
||||
if (!view.Width!.Has<DimAuto> (out _))
|
||||
if (!view.Width.Has<DimAuto> (out _))
|
||||
{
|
||||
view.Width = Dim.Fill ();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
@@ -33,10 +32,10 @@ public class BorderEditor : AdornmentEditor
|
||||
|
||||
Y = Pos.Bottom (SubViews.ToArray () [^1]),
|
||||
Width = Dim.Fill (),
|
||||
Value = ((Border)AdornmentToEdit!)?.LineStyle ?? LineStyle.None,
|
||||
Value = (AdornmentToEdit as Border)?.LineStyle ?? LineStyle.None,
|
||||
BorderStyle = LineStyle.Single,
|
||||
Title = "Border St_yle",
|
||||
SuperViewRendersLineCanvas = true,
|
||||
SuperViewRendersLineCanvas = true
|
||||
};
|
||||
Add (_osBorderStyle);
|
||||
|
||||
@@ -49,7 +48,7 @@ public class BorderEditor : AdornmentEditor
|
||||
|
||||
CheckedState = CheckState.Checked,
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Text = "Title",
|
||||
Text = "Title"
|
||||
};
|
||||
|
||||
_ckbTitle.CheckedStateChanging += OnCkbTitleOnToggle;
|
||||
@@ -62,7 +61,7 @@ public class BorderEditor : AdornmentEditor
|
||||
|
||||
CheckedState = CheckState.Checked,
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Text = "Gradient",
|
||||
Text = "Gradient"
|
||||
};
|
||||
|
||||
_ckbGradient.CheckedStateChanging += OnCkbGradientOnToggle;
|
||||
@@ -72,51 +71,55 @@ public class BorderEditor : AdornmentEditor
|
||||
|
||||
void OnRbBorderStyleOnValueChanged (object? s, EventArgs<LineStyle?> args)
|
||||
{
|
||||
LineStyle prevBorderStyle = AdornmentToEdit!.BorderStyle;
|
||||
if (AdornmentToEdit is not Border border)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Value is { })
|
||||
{
|
||||
((Border)AdornmentToEdit).LineStyle = (LineStyle)args.Value;
|
||||
border.LineStyle = (LineStyle)args.Value;
|
||||
}
|
||||
|
||||
if (((Border)AdornmentToEdit).LineStyle == LineStyle.None)
|
||||
{
|
||||
((Border)AdornmentToEdit).Thickness = new (0);
|
||||
}
|
||||
else if (prevBorderStyle == LineStyle.None && ((Border)AdornmentToEdit).LineStyle != LineStyle.None)
|
||||
{
|
||||
((Border)AdornmentToEdit).Thickness = new (1);
|
||||
}
|
||||
|
||||
((Border)AdornmentToEdit).SetNeedsDraw ();
|
||||
border.SetNeedsDraw ();
|
||||
SetNeedsLayout ();
|
||||
}
|
||||
|
||||
void OnCkbTitleOnToggle (object? _, ResultEventArgs<CheckState> args)
|
||||
{
|
||||
if (AdornmentToEdit is not Border border)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Result == CheckState.Checked)
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit!).Settings |= BorderSettings.Title;
|
||||
border.Settings |= BorderSettings.Title;
|
||||
}
|
||||
else
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit!).Settings &= ~BorderSettings.Title;
|
||||
border.Settings &= ~BorderSettings.Title;
|
||||
}
|
||||
}
|
||||
|
||||
void OnCkbGradientOnToggle (object? _, ResultEventArgs<CheckState> args)
|
||||
{
|
||||
if (AdornmentToEdit is not Border border)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Result == CheckState.Checked)
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit!).Settings |= BorderSettings.Gradient;
|
||||
border.Settings |= BorderSettings.Gradient;
|
||||
}
|
||||
else
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit!).Settings &= ~BorderSettings.Gradient;
|
||||
border.Settings &= ~BorderSettings.Gradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
@@ -21,7 +18,7 @@ public class DimEditor : EditorBase
|
||||
private OptionSelector? _dimOptionSelector;
|
||||
private TextField? _valueEdit;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc/>
|
||||
protected override void OnViewToEditChanged ()
|
||||
{
|
||||
if (ViewToEdit is { })
|
||||
@@ -39,12 +36,11 @@ public class DimEditor : EditorBase
|
||||
return;
|
||||
}
|
||||
|
||||
Dim? dim;
|
||||
dim = Dimension == Dimension.Width ? ViewToEdit.Width : ViewToEdit.Height;
|
||||
Dim dim = Dimension == Dimension.Width ? ViewToEdit.Width : ViewToEdit.Height;
|
||||
|
||||
try
|
||||
{
|
||||
_dimOptionSelector!.Value = _dimNames.IndexOf (_dimNames.First (s => dim!.ToString ().StartsWith (s)));
|
||||
_dimOptionSelector!.Value = _dimNames.IndexOf (_dimNames.First (s => dim.ToString ().StartsWith (s)));
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
@@ -53,31 +49,37 @@ public class DimEditor : EditorBase
|
||||
}
|
||||
|
||||
_valueEdit!.Enabled = false;
|
||||
|
||||
switch (dim)
|
||||
{
|
||||
case DimAbsolute absolute:
|
||||
_valueEdit.Enabled = true;
|
||||
_value = absolute.Size;
|
||||
_valueEdit!.Text = _value.ToString ();
|
||||
|
||||
break;
|
||||
case DimFill fill:
|
||||
var margin = fill.Margin as DimAbsolute;
|
||||
_valueEdit.Enabled = margin is { };
|
||||
_value = margin?.Size ?? 0;
|
||||
_valueEdit!.Text = _value.ToString ();
|
||||
|
||||
break;
|
||||
case DimFunc func:
|
||||
_valueEdit.Enabled = true;
|
||||
_value = func.Fn (null);
|
||||
_valueEdit!.Text = _value.ToString ();
|
||||
|
||||
break;
|
||||
case DimPercent percent:
|
||||
_valueEdit.Enabled = true;
|
||||
_value = percent.Percentage;
|
||||
_valueEdit!.Text = _value.ToString ();
|
||||
|
||||
break;
|
||||
default:
|
||||
_valueEdit!.Text = dim!.ToString ();
|
||||
_valueEdit!.Text = dim.ToString ();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -94,6 +96,7 @@ public class DimEditor : EditorBase
|
||||
Add (label);
|
||||
_dimOptionSelector = new () { X = 0, Y = Pos.Bottom (label), Labels = _optionLabels };
|
||||
_dimOptionSelector.ValueChanged += OnOptionSelectorOnValueChanged;
|
||||
|
||||
_valueEdit = new ()
|
||||
{
|
||||
X = Pos.Right (label) + 1,
|
||||
@@ -102,30 +105,30 @@ public class DimEditor : EditorBase
|
||||
Text = $"{_value}"
|
||||
};
|
||||
|
||||
_valueEdit.Accepting += (s, args) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_value = int.Parse (_valueEdit.Text);
|
||||
DimChanged ();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
args.Handled = true;
|
||||
};
|
||||
_valueEdit.Accepting += (_, args) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_value = int.Parse (_valueEdit.Text);
|
||||
DimChanged ();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
};
|
||||
Add (_valueEdit);
|
||||
|
||||
Add (_dimOptionSelector);
|
||||
|
||||
}
|
||||
|
||||
private void OnOptionSelectorOnValueChanged (object? s, EventArgs<int?> selected) { DimChanged (); }
|
||||
|
||||
// These need to have same order
|
||||
private readonly List<string> _dimNames = ["Absolute", "Auto", "Fill", "Func", "Percent",];
|
||||
private readonly string [] _optionLabels = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)",];
|
||||
// These need to have same order
|
||||
private readonly List<string> _dimNames = ["Absolute", "Auto", "Fill", "Func", "Percent"];
|
||||
private readonly string [] _optionLabels = ["Absolute(n)", "Auto", "Fill(n)", "Func(()=>n)", "Percent(n)"];
|
||||
|
||||
private void DimChanged ()
|
||||
{
|
||||
@@ -136,15 +139,15 @@ public class DimEditor : EditorBase
|
||||
|
||||
try
|
||||
{
|
||||
Dim? dim = _dimOptionSelector!.Value switch
|
||||
{
|
||||
0 => Dim.Absolute (_value),
|
||||
1 => Dim.Auto (),
|
||||
2 => Dim.Fill (_value),
|
||||
3 => Dim.Func (_ => _value),
|
||||
4 => Dim.Percent (_value),
|
||||
_ => Dimension == Dimension.Width ? ViewToEdit.Width : ViewToEdit.Height
|
||||
};
|
||||
Dim dim = _dimOptionSelector!.Value switch
|
||||
{
|
||||
0 => Dim.Absolute (_value),
|
||||
1 => Dim.Auto (),
|
||||
2 => Dim.Fill (_value),
|
||||
3 => Dim.Func (_ => _value),
|
||||
4 => Dim.Percent (_value),
|
||||
_ => Dimension == Dimension.Width ? ViewToEdit.Width : ViewToEdit.Height
|
||||
};
|
||||
|
||||
if (Dimension == Dimension.Width)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
public abstract class EditorBase : View
|
||||
@@ -19,36 +15,21 @@ public abstract class EditorBase : View
|
||||
Orientation = Orientation.Vertical
|
||||
};
|
||||
|
||||
|
||||
TabStop = TabBehavior.TabStop;
|
||||
|
||||
Initialized += OnInitialized;
|
||||
|
||||
void OnInitialized (object? sender, EventArgs e)
|
||||
{
|
||||
if (Border is { })
|
||||
{
|
||||
Border.Add (ExpanderButton);
|
||||
|
||||
if (ExpanderButton.Orientation == Orientation.Vertical)
|
||||
{
|
||||
ExpanderButton.X = Pos.AnchorEnd () - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExpanderButton.Y = Pos.AnchorEnd () - 1;
|
||||
}
|
||||
}
|
||||
|
||||
Application.MouseEvent += ApplicationOnMouseEvent;
|
||||
Application.Navigation!.FocusedChanged += NavigationOnFocusedChanged;
|
||||
Border?.Add (ExpanderButton);
|
||||
|
||||
App!.Mouse.MouseEvent += ApplicationOnMouseEvent;
|
||||
App!.Navigation!.FocusedChanged += NavigationOnFocusedChanged;
|
||||
}
|
||||
|
||||
AddCommand (Command.Accept, () => true);
|
||||
|
||||
SchemeName = "Dialog";
|
||||
|
||||
}
|
||||
|
||||
private readonly ExpanderButton? _expanderButton;
|
||||
@@ -58,15 +39,16 @@ public abstract class EditorBase : View
|
||||
get => _expanderButton;
|
||||
init
|
||||
{
|
||||
if (_expanderButton == value)
|
||||
if (ReferenceEquals (_expanderButton, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_expanderButton = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdatingLayoutSettings { get; private set; } = false;
|
||||
public bool UpdatingLayoutSettings { get; private set; }
|
||||
|
||||
private void View_LayoutComplete (object? sender, LayoutEventArgs e)
|
||||
{
|
||||
@@ -77,7 +59,6 @@ public abstract class EditorBase : View
|
||||
UpdatingLayoutSettings = false;
|
||||
}
|
||||
|
||||
|
||||
private View? _viewToEdit;
|
||||
|
||||
public View? ViewToEdit
|
||||
@@ -90,7 +71,6 @@ public abstract class EditorBase : View
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (value is null && _viewToEdit is { })
|
||||
{
|
||||
_viewToEdit.SubViewsLaidOut -= View_LayoutComplete;
|
||||
@@ -127,7 +107,6 @@ public abstract class EditorBase : View
|
||||
/// </summary>
|
||||
public bool AutoSelectAdornments { get; set; }
|
||||
|
||||
|
||||
private void NavigationOnFocusedChanged (object? sender, EventArgs e)
|
||||
{
|
||||
if (AutoSelectSuperView is null)
|
||||
@@ -135,17 +114,17 @@ public abstract class EditorBase : View
|
||||
return;
|
||||
}
|
||||
|
||||
if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ()))
|
||||
if (ApplicationNavigation.IsInHierarchy (this, App?.Navigation?.GetFocused ()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ApplicationNavigation.IsInHierarchy (AutoSelectSuperView, Application.Navigation!.GetFocused ()))
|
||||
if (!ApplicationNavigation.IsInHierarchy (AutoSelectSuperView, App?.Navigation?.GetFocused ()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewToEdit = Application.Navigation!.GetFocused ();
|
||||
ViewToEdit = App!.Navigation!.GetFocused ();
|
||||
}
|
||||
|
||||
private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e)
|
||||
@@ -177,4 +156,16 @@ public abstract class EditorBase : View
|
||||
ViewToEdit = view;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
if (disposing && App is {})
|
||||
{
|
||||
App.Navigation!.FocusedChanged -= NavigationOnFocusedChanged;
|
||||
App.Mouse.MouseEvent -= ApplicationOnMouseEvent;
|
||||
}
|
||||
|
||||
base.Dispose (disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
@@ -19,8 +18,7 @@ public class EventLog : ListView
|
||||
X = Pos.AnchorEnd ();
|
||||
Y = 0;
|
||||
|
||||
Width = Dim.Func (
|
||||
_ =>
|
||||
Width = Dim.Func (_ =>
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
@@ -43,14 +42,11 @@ public class ExpanderButton : Button
|
||||
|
||||
Orientation = Orientation.Vertical;
|
||||
|
||||
HighlightStates = Terminal.Gui.ViewBase.MouseState.None;
|
||||
HighlightStates = MouseState.In;
|
||||
|
||||
Initialized += ExpanderButton_Initialized;
|
||||
|
||||
EnabledChanged += (sender, args) =>
|
||||
{
|
||||
ShowHide ();
|
||||
};
|
||||
EnabledChanged += (_, _) => { ShowHide (); };
|
||||
}
|
||||
|
||||
private void ShowHide ()
|
||||
@@ -85,7 +81,7 @@ public class ExpanderButton : Button
|
||||
|
||||
if (SuperView is Border { } border)
|
||||
{
|
||||
border.ThicknessChanged += (o, args) => ShowHide ();
|
||||
border.ThicknessChanged += (_, _) => ShowHide ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +107,7 @@ public class ExpanderButton : Button
|
||||
/// <returns>True of the event was cancelled.</returns>
|
||||
protected virtual bool OnOrientationChanging (Orientation newOrientation)
|
||||
{
|
||||
CancelEventArgs<Orientation> args = new CancelEventArgs<Orientation> (in _orientation, ref newOrientation);
|
||||
CancelEventArgs<Orientation> args = new (in _orientation, ref newOrientation);
|
||||
OrientationChanging?.Invoke (this, args);
|
||||
|
||||
if (!args.Cancel)
|
||||
@@ -120,7 +116,7 @@ public class ExpanderButton : Button
|
||||
|
||||
if (Orientation == Orientation.Vertical)
|
||||
{
|
||||
X = Pos.AnchorEnd ();
|
||||
X = Pos.AnchorEnd () - 1;
|
||||
Y = 0;
|
||||
CollapseGlyph = new ('\u21d1'); // ⇑
|
||||
ExpandGlyph = new ('\u21d3'); // ⇓
|
||||
@@ -128,7 +124,7 @@ public class ExpanderButton : Button
|
||||
else
|
||||
{
|
||||
X = 0;
|
||||
Y = Pos.AnchorEnd ();
|
||||
Y = Pos.AnchorEnd () - 1;
|
||||
CollapseGlyph = new ('\u21d0'); // ⇐
|
||||
ExpandGlyph = new ('\u21d2'); // ⇒
|
||||
}
|
||||
@@ -222,12 +218,12 @@ public class ExpanderButton : Button
|
||||
// Collapse
|
||||
if (Orientation == Orientation.Vertical)
|
||||
{
|
||||
_previousDim = superView!.Height!;
|
||||
_previousDim = superView.Height;
|
||||
superView.Height = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_previousDim = superView!.Width!;
|
||||
_previousDim = superView.Width;
|
||||
superView.Width = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +60,6 @@ public class LayoutEditor : EditorBase
|
||||
X = Pos.Right (_xEditor) + 1
|
||||
};
|
||||
|
||||
|
||||
_widthEditor = new ()
|
||||
{
|
||||
Title = "_Width",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
public class MarginEditor : AdornmentEditor
|
||||
@@ -34,7 +32,7 @@ public class MarginEditor : AdornmentEditor
|
||||
_optionsShadow = new ()
|
||||
{
|
||||
X = 0,
|
||||
Y = Pos.Bottom (SubViews.ElementAt(SubViews.Count-1)),
|
||||
Y = Pos.Bottom (SubViews.ElementAt (SubViews.Count - 1)),
|
||||
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Title = "_Shadow",
|
||||
@@ -51,14 +49,14 @@ public class MarginEditor : AdornmentEditor
|
||||
|
||||
Add (_optionsShadow);
|
||||
|
||||
_flagSelectorTransparent = new FlagSelector<ViewportSettingsFlags> ()
|
||||
_flagSelectorTransparent = new FlagSelector<ViewportSettingsFlags>
|
||||
{
|
||||
X = 0,
|
||||
Y = Pos.Bottom (_optionsShadow),
|
||||
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Title = "_ViewportSettings",
|
||||
BorderStyle = LineStyle.Single,
|
||||
BorderStyle = LineStyle.Single
|
||||
};
|
||||
_flagSelectorTransparent.Values = [(int)ViewportSettingsFlags.Transparent, (int)ViewportSettingsFlags.TransparentMouse];
|
||||
_flagSelectorTransparent.Labels = ["Transparent", "TransparentMouse"];
|
||||
@@ -71,11 +69,6 @@ public class MarginEditor : AdornmentEditor
|
||||
_flagSelectorTransparent.Value = (int)((Margin)AdornmentToEdit).ViewportSettings;
|
||||
}
|
||||
|
||||
_flagSelectorTransparent.ValueChanged += (_, args) =>
|
||||
{
|
||||
((Margin)AdornmentToEdit!).ViewportSettings = (ViewportSettingsFlags)args.Value!;
|
||||
};
|
||||
|
||||
|
||||
_flagSelectorTransparent.ValueChanged += (_, args) => { ((Margin)AdornmentToEdit!).ViewportSettings = (ViewportSettingsFlags)args.Value!; };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
@@ -102,7 +99,7 @@ public class PosEditor : EditorBase
|
||||
Text = $"{_value}"
|
||||
};
|
||||
|
||||
_valueEdit.Accepting += (s, args) =>
|
||||
_valueEdit.Accepting += (_, args) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -123,7 +120,7 @@ public class PosEditor : EditorBase
|
||||
|
||||
private void OnOptionSelectorOnValueChanged (object? s, EventArgs<int?> selected) { PosChanged (); }
|
||||
|
||||
// These need to have same order
|
||||
// These need to have same order
|
||||
private readonly List<string> _posNames = ["Absolute", "Align", "AnchorEnd", "Center", "Func", "Percent"];
|
||||
private readonly string [] _optionLabels = ["Absolute(n)", "Align", "AnchorEnd", "Center", "Func(()=>n)", "Percent(n)"];
|
||||
|
||||
@@ -136,7 +133,7 @@ public class PosEditor : EditorBase
|
||||
|
||||
try
|
||||
{
|
||||
Pos? pos = _posOptionSelector!.Value switch
|
||||
Pos pos = _posOptionSelector!.Value switch
|
||||
{
|
||||
0 => Pos.Absolute (_value),
|
||||
1 => Pos.Align (Alignment.Start),
|
||||
|
||||
@@ -20,7 +20,7 @@ public class ViewPropertiesEditor : EditorBase
|
||||
CheckedState = ViewToEdit is { } ? ViewToEdit.CanFocus ? CheckState.Checked : CheckState.UnChecked : CheckState.UnChecked
|
||||
};
|
||||
|
||||
_canFocusCheckBox.CheckedStateChanged += (s, args) =>
|
||||
_canFocusCheckBox.CheckedStateChanged += (_, _) =>
|
||||
{
|
||||
if (ViewToEdit is { })
|
||||
{
|
||||
@@ -37,7 +37,7 @@ public class ViewPropertiesEditor : EditorBase
|
||||
CheckedState = ViewToEdit is { } ? ViewToEdit.Enabled ? CheckState.Checked : CheckState.UnChecked : CheckState.UnChecked
|
||||
};
|
||||
|
||||
_enabledCheckBox.CheckedStateChanged += (s, args) =>
|
||||
_enabledCheckBox.CheckedStateChanged += (_, _) =>
|
||||
{
|
||||
if (ViewToEdit is { })
|
||||
{
|
||||
@@ -55,13 +55,13 @@ public class ViewPropertiesEditor : EditorBase
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
|
||||
_orientationOptionSelector.ValueChanged += (s, selected) =>
|
||||
{
|
||||
if (ViewToEdit is IOrientation orientatedView)
|
||||
{
|
||||
orientatedView.Orientation = _orientationOptionSelector.Value!.Value;
|
||||
}
|
||||
};
|
||||
_orientationOptionSelector.ValueChanged += (_, _) =>
|
||||
{
|
||||
if (ViewToEdit is IOrientation orientatedView)
|
||||
{
|
||||
orientatedView.Orientation = _orientationOptionSelector.Value!.Value;
|
||||
}
|
||||
};
|
||||
Add (label, _orientationOptionSelector);
|
||||
|
||||
label = new () { X = 0, Y = Pos.Bottom (_orientationOptionSelector), Text = "Text:" };
|
||||
@@ -75,7 +75,7 @@ public class ViewPropertiesEditor : EditorBase
|
||||
Text = "This is demo text"
|
||||
};
|
||||
|
||||
_text.ContentsChanged += (s, e) =>
|
||||
_text.ContentsChanged += (_, _) =>
|
||||
{
|
||||
if (ViewToEdit is { })
|
||||
{
|
||||
@@ -90,15 +90,7 @@ public class ViewPropertiesEditor : EditorBase
|
||||
|
||||
public string DemoText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_text is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _text!.Text;
|
||||
}
|
||||
get => _text is null ? string.Empty : _text!.Text;
|
||||
set => _text!.Text = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
@@ -60,8 +58,8 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
: CheckState.UnChecked;
|
||||
|
||||
_cbTransparentMouse!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse)
|
||||
? CheckState.Checked
|
||||
: CheckState.UnChecked;
|
||||
? CheckState.Checked
|
||||
: CheckState.UnChecked;
|
||||
|
||||
_cbVerticalScrollBar!.CheckedState = ViewToEdit.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked;
|
||||
_cbAutoShowVerticalScrollBar!.CheckedState = ViewToEdit.VerticalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked;
|
||||
@@ -115,27 +113,27 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
|
||||
Add (_cbAllowXGreaterThanContentWidth);
|
||||
|
||||
void AllowNegativeXToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AllowNegativeXToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeX;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.AllowNegativeX;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeX;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.AllowNegativeX;
|
||||
}
|
||||
}
|
||||
|
||||
void AllowXGreaterThanContentWidthToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AllowXGreaterThanContentWidthToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowXGreaterThanContentWidth;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.AllowXGreaterThanContentWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowXGreaterThanContentWidth;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.AllowXGreaterThanContentWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,27 +151,27 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
|
||||
Add (_cbAllowYGreaterThanContentHeight);
|
||||
|
||||
void AllowNegativeYToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AllowNegativeYToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeY;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.AllowNegativeY;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeY;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.AllowNegativeY;
|
||||
}
|
||||
}
|
||||
|
||||
void AllowYGreaterThanContentHeightToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AllowYGreaterThanContentHeightToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowYGreaterThanContentHeight;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.AllowYGreaterThanContentHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowYGreaterThanContentHeight;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.AllowYGreaterThanContentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,17 +191,16 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_contentSizeWidth.ValueChanging += ContentSizeWidthValueChanged;
|
||||
|
||||
void ContentSizeWidthValueChanged (object? sender, CancelEventArgs<int> e)
|
||||
void ContentSizeWidthValueChanged (object? sender, CancelEventArgs<int> cea)
|
||||
{
|
||||
if (e.NewValue < 0)
|
||||
if (cea.NewValue < 0)
|
||||
{
|
||||
e.Cancel = true;
|
||||
cea.Cancel = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// BUGBUG: set_ContentSize is supposed to be `protected`.
|
||||
ViewToEdit!.SetContentSize (ViewToEdit.GetContentSize () with { Width = e.NewValue });
|
||||
ViewToEdit!.SetContentSize (ViewToEdit.GetContentSize () with { Width = cea.NewValue });
|
||||
}
|
||||
|
||||
var labelComma = new Label
|
||||
@@ -221,17 +218,16 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_contentSizeHeight.ValueChanging += ContentSizeHeightValueChanged;
|
||||
|
||||
void ContentSizeHeightValueChanged (object? sender, CancelEventArgs<int> e)
|
||||
void ContentSizeHeightValueChanged (object? sender, CancelEventArgs<int> cea)
|
||||
{
|
||||
if (e.NewValue < 0)
|
||||
if (cea.NewValue < 0)
|
||||
{
|
||||
e.Cancel = true;
|
||||
cea.Cancel = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// BUGBUG: set_ContentSize is supposed to be `protected`.
|
||||
ViewToEdit?.SetContentSize (ViewToEdit.GetContentSize () with { Height = e.NewValue });
|
||||
ViewToEdit?.SetContentSize (ViewToEdit.GetContentSize () with { Height = cea.NewValue });
|
||||
}
|
||||
|
||||
_cbClearContentOnly = new ()
|
||||
@@ -243,15 +239,15 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbClearContentOnly.CheckedStateChanging += ClearContentOnlyToggle;
|
||||
|
||||
void ClearContentOnlyToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void ClearContentOnlyToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.ClearContentOnly;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.ClearContentOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.ClearContentOnly;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.ClearContentOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,15 +260,15 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbClipContentOnly.CheckedStateChanging += ClipContentOnlyToggle;
|
||||
|
||||
void ClipContentOnlyToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void ClipContentOnlyToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.ClipContentOnly;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.ClipContentOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.ClipContentOnly;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.ClipContentOnly;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,15 +281,15 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbTransparent.CheckedStateChanging += TransparentToggle;
|
||||
|
||||
void TransparentToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void TransparentToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.Transparent;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,15 +302,15 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbTransparentMouse.CheckedStateChanging += TransparentMouseToggle;
|
||||
|
||||
void TransparentMouseToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void TransparentMouseToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
if (e.Result == CheckState.Checked)
|
||||
if (rea.Result == CheckState.Checked)
|
||||
{
|
||||
ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse;
|
||||
ViewToEdit!.ViewportSettings |= ViewportSettingsFlags.TransparentMouse;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse;
|
||||
ViewToEdit!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,9 +323,9 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbVerticalScrollBar.CheckedStateChanging += VerticalScrollBarToggle;
|
||||
|
||||
void VerticalScrollBarToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void VerticalScrollBarToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
ViewToEdit!.VerticalScrollBar.Visible = e.Result == CheckState.Checked;
|
||||
ViewToEdit!.VerticalScrollBar.Visible = rea.Result == CheckState.Checked;
|
||||
}
|
||||
|
||||
_cbAutoShowVerticalScrollBar = new ()
|
||||
@@ -341,9 +337,9 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbAutoShowVerticalScrollBar.CheckedStateChanging += AutoShowVerticalScrollBarToggle;
|
||||
|
||||
void AutoShowVerticalScrollBarToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AutoShowVerticalScrollBarToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
ViewToEdit!.VerticalScrollBar.AutoShow = e.Result == CheckState.Checked;
|
||||
ViewToEdit!.VerticalScrollBar.AutoShow = rea.Result == CheckState.Checked;
|
||||
}
|
||||
|
||||
_cbHorizontalScrollBar = new ()
|
||||
@@ -355,9 +351,9 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbHorizontalScrollBar.CheckedStateChanging += HorizontalScrollBarToggle;
|
||||
|
||||
void HorizontalScrollBarToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void HorizontalScrollBarToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
ViewToEdit!.HorizontalScrollBar.Visible = e.Result == CheckState.Checked;
|
||||
ViewToEdit!.HorizontalScrollBar.Visible = rea.Result == CheckState.Checked;
|
||||
}
|
||||
|
||||
_cbAutoShowHorizontalScrollBar = new ()
|
||||
@@ -369,9 +365,9 @@ public sealed class ViewportSettingsEditor : EditorBase
|
||||
};
|
||||
_cbAutoShowHorizontalScrollBar.CheckedStateChanging += AutoShowHorizontalScrollBarToggle;
|
||||
|
||||
void AutoShowHorizontalScrollBarToggle (object? sender, ResultEventArgs<CheckState> e)
|
||||
void AutoShowHorizontalScrollBarToggle (object? sender, ResultEventArgs<CheckState> rea)
|
||||
{
|
||||
ViewToEdit!.HorizontalScrollBar.AutoShow = e.Result == CheckState.Checked;
|
||||
ViewToEdit!.HorizontalScrollBar.AutoShow = rea.Result == CheckState.Checked;
|
||||
}
|
||||
|
||||
Add (
|
||||
|
||||
@@ -62,6 +62,22 @@ public class ShadowStyles : Scenario
|
||||
shadowWindow.Add (buttonInWin);
|
||||
app.Add (shadowWindow);
|
||||
|
||||
Window shadowWindow2 = new ()
|
||||
{
|
||||
|
||||
Id = "shadowWindow2",
|
||||
X = Pos.Right (editor) + 10,
|
||||
Y = 10,
|
||||
Width = Dim.Percent (30),
|
||||
Height = Dim.Percent (30),
|
||||
Title = "Shadow Window #2",
|
||||
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped,
|
||||
BorderStyle = LineStyle.Double,
|
||||
ShadowStyle = ShadowStyle.Transparent,
|
||||
};
|
||||
app.Add (shadowWindow2);
|
||||
|
||||
|
||||
var button = new Button
|
||||
{
|
||||
Id = "button",
|
||||
@@ -69,6 +85,7 @@ public class ShadowStyles : Scenario
|
||||
Y = Pos.Center (), Text = "Button",
|
||||
ShadowStyle = ShadowStyle.Opaque
|
||||
};
|
||||
button.Accepting += ButtonOnAccepting;
|
||||
|
||||
ColorPicker colorPicker = new ()
|
||||
{
|
||||
@@ -77,12 +94,12 @@ public class ShadowStyles : Scenario
|
||||
Id = "colorPicker16",
|
||||
X = Pos.Center (),
|
||||
Y = Pos.AnchorEnd (),
|
||||
Width = Dim.Percent(80),
|
||||
Width = Dim.Percent (80),
|
||||
};
|
||||
colorPicker.ColorChanged += (sender, args) =>
|
||||
{
|
||||
var normal = app.GetScheme ().Normal;
|
||||
app.SetScheme (app.GetScheme() with {Normal = new Attribute(normal.Foreground, args.Result)});
|
||||
app.SetScheme (app.GetScheme () with { Normal = new Attribute (normal.Foreground, args.Result) });
|
||||
};
|
||||
app.Add (button, colorPicker);
|
||||
|
||||
@@ -96,4 +113,10 @@ public class ShadowStyles : Scenario
|
||||
Application.Shutdown ();
|
||||
|
||||
}
|
||||
|
||||
private void ButtonOnAccepting (object sender, CommandEventArgs e)
|
||||
{
|
||||
MessageBox.Query ((sender as View)?.App, "Hello", "You pushed the button!");
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ public class ViewportSettings : Scenario
|
||||
|
||||
var adornmentsEditor = new AdornmentsEditor
|
||||
{
|
||||
BorderStyle = LineStyle.Single,
|
||||
X = Pos.AnchorEnd (),
|
||||
AutoSelectViewToEdit = true,
|
||||
ShowViewIdentifier = true
|
||||
@@ -224,6 +225,7 @@ public class ViewportSettings : Scenario
|
||||
view.Initialized += (s, e) =>
|
||||
{
|
||||
viewportSettingsEditor.ViewToEdit = view;
|
||||
adornmentsEditor.ViewToEdit = view;
|
||||
};
|
||||
view.SetFocus ();
|
||||
Application.Run (app);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System.Text;
|
||||
|
||||
@@ -24,6 +24,27 @@ public sealed class WideGlyphs : Scenario
|
||||
BorderStyle = LineStyle.None
|
||||
};
|
||||
|
||||
// Add Editors
|
||||
|
||||
AdornmentsEditor adornmentsEditor = new ()
|
||||
{
|
||||
BorderStyle = LineStyle.Single,
|
||||
X = Pos.AnchorEnd (),
|
||||
AutoSelectViewToEdit = true,
|
||||
AutoSelectAdornments = false,
|
||||
ShowViewIdentifier = true
|
||||
};
|
||||
appWindow.Add (adornmentsEditor);
|
||||
|
||||
ViewportSettingsEditor viewportSettingsEditor = new ()
|
||||
{
|
||||
BorderStyle = LineStyle.Single,
|
||||
Y = Pos.AnchorEnd (),
|
||||
X = Pos.AnchorEnd (),
|
||||
AutoSelectViewToEdit = true,
|
||||
};
|
||||
appWindow.Add (viewportSettingsEditor);
|
||||
|
||||
// Build the array of codepoints once when subviews are laid out
|
||||
appWindow.SubViewsLaidOut += (s, _) =>
|
||||
{
|
||||
@@ -53,7 +74,7 @@ public sealed class WideGlyphs : Scenario
|
||||
// Fill the window with the pre-built codepoints array
|
||||
// For detailed documentation on the draw code flow from Application.Run to this event,
|
||||
// see WideGlyphs.DrawFlow.md in this directory
|
||||
appWindow.DrawingContent += (s, _) =>
|
||||
appWindow.DrawingContent += (s, e) =>
|
||||
{
|
||||
View? view = s as View;
|
||||
if (view is null || _codepoints is null)
|
||||
@@ -69,28 +90,16 @@ public sealed class WideGlyphs : Scenario
|
||||
Rune codepoint = _codepoints [r, c];
|
||||
if (codepoint != default (Rune))
|
||||
{
|
||||
view.AddRune (c, r, codepoint);
|
||||
view.Move (c, r);
|
||||
Attribute attr = view.GetAttributeForRole (VisualRole.Normal);
|
||||
view.SetAttribute (attr with { Background = attr.Background + (r * 5) });
|
||||
view.AddRune (codepoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
e.DrawContext?.AddDrawnRectangle (view.Viewport);
|
||||
};
|
||||
|
||||
Line verticalLineAtEven = new ()
|
||||
{
|
||||
X = 10,
|
||||
Orientation = Orientation.Vertical,
|
||||
Length = Dim.Fill ()
|
||||
};
|
||||
appWindow.Add (verticalLineAtEven);
|
||||
|
||||
Line verticalLineAtOdd = new ()
|
||||
{
|
||||
X = 25,
|
||||
Orientation = Orientation.Vertical,
|
||||
Length = Dim.Fill ()
|
||||
};
|
||||
appWindow.Add (verticalLineAtOdd);
|
||||
|
||||
View arrangeableViewAtEven = new ()
|
||||
{
|
||||
CanFocus = true,
|
||||
@@ -99,16 +108,19 @@ public sealed class WideGlyphs : Scenario
|
||||
Y = 5,
|
||||
Width = 15,
|
||||
Height = 5,
|
||||
//BorderStyle = LineStyle.Dashed,
|
||||
//BorderStyle = LineStyle.Dashed
|
||||
};
|
||||
|
||||
arrangeableViewAtEven.SetScheme (new () { Normal = new (Color.Black, Color.Green) });
|
||||
|
||||
// Proves it's not LineCanvas related
|
||||
arrangeableViewAtEven!.Border!.Thickness = new (1);
|
||||
arrangeableViewAtEven.Border.Add(new View () { Height = Dim.Auto(), Width = Dim.Auto(), Text = "Even" });
|
||||
arrangeableViewAtEven.Border.Add (new View () { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Even" });
|
||||
appWindow.Add (arrangeableViewAtEven);
|
||||
|
||||
View arrangeableViewAtOdd = new ()
|
||||
Button arrangeableViewAtOdd = new ()
|
||||
{
|
||||
Title = $"你 {Glyphs.Apple}",
|
||||
CanFocus = true,
|
||||
Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
|
||||
X = 31,
|
||||
@@ -116,7 +128,12 @@ public sealed class WideGlyphs : Scenario
|
||||
Width = 15,
|
||||
Height = 5,
|
||||
BorderStyle = LineStyle.Dashed,
|
||||
SchemeName = "error"
|
||||
};
|
||||
arrangeableViewAtOdd.Accepting += (sender, args) =>
|
||||
{
|
||||
MessageBox.Query ((sender as View)?.App, "Button Pressed", "You Pressed it!");
|
||||
};
|
||||
appWindow.Add (arrangeableViewAtOdd);
|
||||
|
||||
var superView = new View
|
||||
@@ -124,11 +141,14 @@ public sealed class WideGlyphs : Scenario
|
||||
CanFocus = true,
|
||||
X = 30, // on an even column to start
|
||||
Y = Pos.Center (),
|
||||
Width = Dim.Auto () + 4,
|
||||
Height = Dim.Auto () + 1,
|
||||
Width = Dim.Auto (),
|
||||
Height = Dim.Auto (),
|
||||
BorderStyle = LineStyle.Single,
|
||||
Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable
|
||||
Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
|
||||
ShadowStyle = ShadowStyle.Transparent,
|
||||
};
|
||||
superView.Margin!.ShadowSize = superView.Margin!.ShadowSize with { Width = 2 };
|
||||
|
||||
|
||||
Rune codepoint = Glyphs.Apple;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ internal partial class ApplicationImpl
|
||||
public event EventHandler<EventArgs<IApplication?>>? Iteration;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RaiseIteration () { Iteration?.Invoke (null, new (this)); }
|
||||
public void RaiseIteration () { Iteration?.Invoke (this, new (this)); }
|
||||
|
||||
#endregion Main Loop Iteration
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public class ApplicationNavigation
|
||||
|
||||
_focused = value;
|
||||
|
||||
FocusedChanged?.Invoke (null, EventArgs.Empty);
|
||||
FocusedChanged?.Invoke (this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -49,6 +49,7 @@ public static class CWPEventHelper
|
||||
return false;
|
||||
}
|
||||
|
||||
// BUGBUG: This should pass this not null; need to test
|
||||
eventHandler.Invoke (null, args);
|
||||
return args.Handled;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public static class CWPPropertyHelper
|
||||
}
|
||||
}
|
||||
|
||||
// BUGBUG: This should pass this not null; need to test
|
||||
changingEvent?.Invoke (null, args);
|
||||
|
||||
if (args.Handled)
|
||||
@@ -100,13 +101,14 @@ public static class CWPPropertyHelper
|
||||
}
|
||||
|
||||
finalValue = args.NewValue;
|
||||
|
||||
|
||||
// Do the work (set backing field, update related properties, etc.) BEFORE raising Changed events
|
||||
doWork (finalValue);
|
||||
|
||||
|
||||
ValueChangedEventArgs<T> changedArgs = new (currentValue, finalValue);
|
||||
currentValue = finalValue;
|
||||
onChanged?.Invoke (changedArgs);
|
||||
// BUGBUG: This should pass this not null; need to test
|
||||
changedEvent?.Invoke (null, changedArgs);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -53,6 +53,7 @@ public static class CWPWorkflowHelper
|
||||
return true;
|
||||
}
|
||||
|
||||
// BUGBUG: This should pass this not null; need to test
|
||||
eventHandler?.Invoke (null, args);
|
||||
if (args.Handled)
|
||||
{
|
||||
@@ -112,6 +113,7 @@ public static class CWPWorkflowHelper
|
||||
return args.Result!;
|
||||
}
|
||||
|
||||
// BUGBUG: This should pass this not null; need to test
|
||||
eventHandler?.Invoke (null, args);
|
||||
|
||||
if (!args.Handled)
|
||||
|
||||
@@ -160,7 +160,7 @@ internal class KeyboardImpl : IKeyboard, IDisposable
|
||||
//#endif
|
||||
|
||||
// TODO: This should match standard event patterns
|
||||
KeyDown?.Invoke (null, key);
|
||||
KeyDown?.Invoke (this, key);
|
||||
|
||||
if (key.Handled)
|
||||
{
|
||||
@@ -216,7 +216,7 @@ internal class KeyboardImpl : IKeyboard, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
KeyUp?.Invoke (null, key);
|
||||
KeyUp?.Invoke (this, key);
|
||||
|
||||
if (key.Handled)
|
||||
{
|
||||
|
||||
@@ -86,7 +86,7 @@ internal class MouseImpl : IMouse, IDisposable
|
||||
mouseEvent.View = deepestViewUnderMouse;
|
||||
}
|
||||
|
||||
MouseEvent?.Invoke (null, mouseEvent);
|
||||
MouseEvent?.Invoke (this, mouseEvent);
|
||||
|
||||
if (mouseEvent.Handled)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,11 @@ public class Glyphs
|
||||
// IMPORTANT: Configuration Manager test SaveDefaults uses this class to generate the default config file
|
||||
// IMPORTANT: in ./UnitTests/bin/Debug/netX.0/config.json
|
||||
|
||||
/// <summary>Unicode replacement character; used by Drivers when rendering in cases where a wide glyph can't
|
||||
/// be output because it would be clipped. Defaults to ' ' (Space).</summary>
|
||||
[ConfigurationProperty (Scope = typeof (ThemeScope))]
|
||||
public static Rune WideGlyphReplacement { get; set; } = (Rune)' ';
|
||||
|
||||
/// <summary>File icon. Defaults to ☰ (Trigram For Heaven)</summary>
|
||||
[ConfigurationProperty (Scope = typeof (ThemeScope))]
|
||||
public static Rune File { get; set; } = (Rune)'☰';
|
||||
|
||||
@@ -146,6 +146,9 @@ internal class DriverImpl : IDriver
|
||||
|
||||
private readonly IOutput _output;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IOutputBuffer GetOutputBuffer () => OutputBuffer;
|
||||
|
||||
public IOutput GetOutput () => _output;
|
||||
|
||||
private readonly IInputProcessor _inputProcessor;
|
||||
@@ -330,9 +333,6 @@ internal class DriverImpl : IDriver
|
||||
/// <inheritdoc/>
|
||||
public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Attribute SetAttribute (Attribute newAttribute)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,13 @@ public interface IDriver : IDisposable
|
||||
IInputProcessor GetInputProcessor ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output handler responsible for writing to the terminal.
|
||||
/// Gets the <see cref="IOutputBuffer"/> containing the buffered screen contents.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IOutputBuffer GetOutputBuffer ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IOutput"/> responsible for writing to the terminal.
|
||||
/// </summary>
|
||||
IOutput GetOutput ();
|
||||
|
||||
@@ -257,14 +263,6 @@ public interface IDriver : IDisposable
|
||||
/// <param name="rune">The Rune used to fill the rectangle</param>
|
||||
void FillRect (Rectangle rect, Rune rune = default);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="c"></param>
|
||||
void FillRect (Rectangle rect, char c);
|
||||
|
||||
/// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
|
||||
/// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
|
||||
/// <param name="c">C.</param>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the desired screen state for console rendering. This interface provides methods for building up
|
||||
@@ -128,4 +127,15 @@ public interface IOutputBuffer
|
||||
/// Changing this may have unexpected consequences.
|
||||
/// </summary>
|
||||
int Top { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the replacement character that will be used when a wide glyph (double-width character) cannot fit in the
|
||||
/// available space.
|
||||
/// If not set, the default will be <see cref="Glyphs.WideGlyphReplacement"/>.
|
||||
/// </summary>
|
||||
/// <param name="column1ReplacementChar">
|
||||
/// The character used when the first column of a wide character is invalid (for example, when it is overlapped by the
|
||||
/// trailing half of a previous wide character).
|
||||
/// </param>
|
||||
void SetWideGlyphReplacement (Rune column1ReplacementChar);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,13 @@ public abstract class OutputBase
|
||||
Cell cell = buffer.Contents [row, col];
|
||||
buffer.Contents [row, col].IsDirty = false;
|
||||
AppendCellAnsi (cell, outputStringBuilder, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth);
|
||||
|
||||
if (col != lastCol)
|
||||
{
|
||||
// Was a wide grapheme so mark clean next cell
|
||||
// See https://github.com/gui-cs/Terminal.Gui/issues/4466
|
||||
buffer.Contents [row, col].IsDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,14 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// <summary>The topmost row in the terminal.</summary>
|
||||
public virtual int Top { get; set; } = 0;
|
||||
|
||||
private Rune _column1ReplacementChar = Glyphs.WideGlyphReplacement;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetWideGlyphReplacement (Rune column1ReplacementChar)
|
||||
{
|
||||
_column1ReplacementChar = column1ReplacementChar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates which lines have been modified and need to be redrawn.
|
||||
/// </summary>
|
||||
@@ -86,7 +94,7 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
get => _clip;
|
||||
set
|
||||
{
|
||||
if (_clip == value)
|
||||
if (ReferenceEquals (_clip, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -94,10 +102,7 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
_clip = value;
|
||||
|
||||
// Don't ever let Clip be bigger than Screen
|
||||
if (_clip is { })
|
||||
{
|
||||
_clip.Intersect (Screen);
|
||||
}
|
||||
_clip?.Intersect (Screen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside the <see cref="Clip"/> or screen
|
||||
/// dimensions defined by <see cref="Cols"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -156,25 +161,19 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
Clip ??= new (Screen);
|
||||
Rectangle clipRect = Clip!.GetBounds ();
|
||||
|
||||
string text = grapheme;
|
||||
int textWidth = -1;
|
||||
int printableGraphemeWidth = -1;
|
||||
|
||||
lock (Contents)
|
||||
{
|
||||
bool validLocation = IsValidLocation (text, Col, Row);
|
||||
|
||||
if (validLocation)
|
||||
if (IsValidLocation (grapheme, Col, Row))
|
||||
{
|
||||
text = text.MakePrintable ();
|
||||
textWidth = text.GetColumns ();
|
||||
|
||||
// Set attribute and mark dirty for current cell
|
||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
||||
Contents [Row, Col].IsDirty = true;
|
||||
SetAttributeAndDirty (Col, Row);
|
||||
InvalidateOverlappedWideGlyph (Col, Row);
|
||||
|
||||
InvalidateOverlappedWideGlyph ();
|
||||
|
||||
WriteGraphemeByWidth (text, textWidth, clipRect);
|
||||
string printableGrapheme = grapheme.MakePrintable ();
|
||||
printableGraphemeWidth = printableGrapheme.GetColumns ();
|
||||
WriteGraphemeByWidth (Col, Row, printableGrapheme, printableGraphemeWidth, clipRect);
|
||||
|
||||
DirtyLines [Row] = true;
|
||||
}
|
||||
@@ -183,97 +182,121 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
// Keep Col/Row updates inside the lock to prevent race conditions
|
||||
Col++;
|
||||
|
||||
if (textWidth > 1)
|
||||
if (printableGraphemeWidth > 1)
|
||||
{
|
||||
// Skip the second column of a wide character
|
||||
// IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
|
||||
// See: https://github.com/gui-cs/Terminal.Gui/issues/4258
|
||||
// See issue: https://github.com/gui-cs/Terminal.Gui/issues/4492
|
||||
// Test: AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly
|
||||
// Test: AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip
|
||||
if (Clip.Contains (Col, Row))
|
||||
{
|
||||
// IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
|
||||
// See: https://github.com/gui-cs/Terminal.Gui/issues/4258
|
||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
||||
}
|
||||
|
||||
// Advance cursor again for wide character
|
||||
Col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we're writing at an odd column and there's a wide glyph to our left,
|
||||
/// INTERNAL: Helper to set the attribute and mark the cell as dirty.
|
||||
/// </summary>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
private void SetAttributeAndDirty (int col, int row)
|
||||
{
|
||||
Contents! [row, col].Attribute = CurrentAttribute;
|
||||
Contents [row, col].IsDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL: If we're writing at an odd column and there's a wide glyph to our left,
|
||||
/// invalidate it since we're overwriting the second half.
|
||||
/// </summary>
|
||||
private void InvalidateOverlappedWideGlyph ()
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
private void InvalidateOverlappedWideGlyph (int col, int row)
|
||||
{
|
||||
if (Col > 0 && Contents! [Row, Col - 1].Grapheme.GetColumns () > 1)
|
||||
if (col > 0 && Contents! [row, col - 1].Grapheme.GetColumns () > 1)
|
||||
{
|
||||
Contents [Row, Col - 1].Grapheme = Rune.ReplacementChar.ToString ();
|
||||
Contents [Row, Col - 1].IsDirty = true;
|
||||
Contents [row, col - 1].Grapheme = _column1ReplacementChar.ToString ();
|
||||
Contents [row, col - 1].IsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a grapheme to the buffer based on its width (0, 1, or 2 columns).
|
||||
/// INTERNAL: Writes a Grapheme to the buffer based on its width (0, 1, or 2 columns).
|
||||
/// </summary>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="text">The printable text to write.</param>
|
||||
/// <param name="textWidth">The column width of the text.</param>
|
||||
/// <param name="clipRect">The clipping rectangle.</param>
|
||||
private void WriteGraphemeByWidth (string text, int textWidth, Rectangle clipRect)
|
||||
private void WriteGraphemeByWidth (int col, int row, string text, int textWidth, Rectangle clipRect)
|
||||
{
|
||||
switch (textWidth)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
WriteSingleWidthGrapheme (text, clipRect);
|
||||
WriteGrapheme (col, row, text, clipRect);
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
WriteWideGrapheme (text);
|
||||
WriteWideGrapheme (col, row, text);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Negative width or non-spacing character (shouldn't normally occur)
|
||||
Contents! [Row, Col].Grapheme = " ";
|
||||
Contents [Row, Col].IsDirty = false;
|
||||
Contents! [row, col].Grapheme = " ";
|
||||
Contents [row, col].IsDirty = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single-width character (0 or 1 column wide).
|
||||
/// INTERNAL: Writes a (0 or 1 column wide) Grapheme.
|
||||
/// </summary>
|
||||
private void WriteSingleWidthGrapheme (string text, Rectangle clipRect)
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="grapheme">The single-width Grapheme to write.</param>
|
||||
/// <param name="clipRect">The clipping rectangle.</param>
|
||||
private void WriteGrapheme (int col, int row, string grapheme, Rectangle clipRect)
|
||||
{
|
||||
Contents! [Row, Col].Grapheme = text;
|
||||
Debug.Assert (grapheme.GetColumns () < 2);
|
||||
Contents! [row, col].Grapheme = grapheme;
|
||||
|
||||
// Mark the next cell as dirty to ensure proper rendering of adjacent content
|
||||
if (Col < clipRect.Right - 1 && Col + 1 < Cols)
|
||||
if (col < clipRect.Right - 1 && col + 1 < Cols)
|
||||
{
|
||||
Contents [Row, Col + 1].IsDirty = true;
|
||||
Contents [row, col + 1].IsDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a wide character (2 columns wide) handling clipping and partial overlap cases.
|
||||
/// INTERNAL: Writes a wide Grapheme (2 columns wide) handling clipping and partial overlap cases.
|
||||
/// </summary>
|
||||
private void WriteWideGrapheme (string text)
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="grapheme">The wide Grapheme to write.</param>
|
||||
private void WriteWideGrapheme (int col, int row, string grapheme)
|
||||
{
|
||||
if (!Clip!.Contains (Col + 1, Row))
|
||||
Debug.Assert (grapheme.GetColumns () == 2);
|
||||
if (!Clip!.Contains (col + 1, row))
|
||||
{
|
||||
// Second column is outside clip - can't fit wide char here
|
||||
Contents! [Row, Col].Grapheme = Rune.ReplacementChar.ToString ();
|
||||
}
|
||||
else if (!Clip.Contains (Col, Row))
|
||||
{
|
||||
// First column is outside clip but second isn't
|
||||
// Mark second column as replacement to indicate partial overlap
|
||||
if (Col + 1 < Cols)
|
||||
{
|
||||
Contents! [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString ();
|
||||
}
|
||||
Contents! [row, col].Grapheme = _column1ReplacementChar.ToString ();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Both columns are in bounds - write the wide character
|
||||
// It will naturally render across both columns when output to the terminal
|
||||
Contents! [Row, Col].Grapheme = text;
|
||||
Contents! [row, col].Grapheme = grapheme;
|
||||
|
||||
// DO NOT modify column N+1 here!
|
||||
// The wide glyph will naturally render across both columns.
|
||||
@@ -288,7 +311,7 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
{
|
||||
Contents = new Cell [Rows, Cols];
|
||||
|
||||
//CONCURRENCY: Unsynchronized access to Clip isn't safe.
|
||||
// CONCURRENCY: Unsynchronized access to Clip isn't safe.
|
||||
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
|
||||
Clip = new (Screen);
|
||||
|
||||
@@ -311,9 +334,6 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
DirtyLines [row] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Who uses this and why? I am removing for now - this class is a state class not an events class
|
||||
//ClearedContents?.Invoke (this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Tests whether the specified coordinate are valid for drawing the specified Text.</summary>
|
||||
@@ -342,8 +362,9 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// <inheritdoc/>
|
||||
public void FillRect (Rectangle rect, Rune rune)
|
||||
{
|
||||
Rectangle clipBounds = Clip?.GetBounds () ?? Screen;
|
||||
// BUGBUG: This should be a method on Region
|
||||
rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
|
||||
rect = Rectangle.Intersect (rect, clipBounds);
|
||||
|
||||
lock (Contents!)
|
||||
{
|
||||
@@ -356,11 +377,12 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
continue;
|
||||
}
|
||||
|
||||
Contents [r, c] = new ()
|
||||
{
|
||||
Grapheme = rune != default (Rune) ? rune.ToString () : " ",
|
||||
Attribute = CurrentAttribute, IsDirty = true
|
||||
};
|
||||
// We could call AddGrapheme here, but that would acquire the lock again.
|
||||
// So we inline the logic instead.
|
||||
SetAttributeAndDirty (c, r);
|
||||
InvalidateOverlappedWideGlyph (c, r);
|
||||
string grapheme = rune != default (Rune) ? rune.ToString () : " ";
|
||||
WriteGraphemeByWidth (c, r, grapheme, grapheme.GetColumns (), clipBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +401,6 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make internal once Menu is upgraded
|
||||
/// <summary>
|
||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
||||
/// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
@@ -393,9 +414,8 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
public virtual void Move (int col, int row)
|
||||
public void Move (int col, int row)
|
||||
{
|
||||
//Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
|
||||
Col = col;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui.ViewBase;
|
||||
|
||||
/// <summary>The Margin for a <see cref="View"/>. Accessed via <see cref="View.Margin"/></summary>
|
||||
@@ -21,8 +16,6 @@ namespace Terminal.Gui.ViewBase;
|
||||
/// </remarks>
|
||||
public class Margin : Adornment
|
||||
{
|
||||
private const int SHADOW_WIDTH = 1;
|
||||
private const int SHADOW_HEIGHT = 1;
|
||||
private const int PRESS_MOVE_HORIZONTAL = 1;
|
||||
private const int PRESS_MOVE_VERTICAL = 0;
|
||||
|
||||
@@ -35,6 +28,7 @@ public class Margin : Adornment
|
||||
public Margin (View parent) : base (parent)
|
||||
{
|
||||
SubViewLayout += Margin_LayoutStarted;
|
||||
ThicknessChanged += OnThicknessChanged;
|
||||
|
||||
// Margin should not be focusable
|
||||
CanFocus = false;
|
||||
@@ -46,6 +40,15 @@ public class Margin : Adornment
|
||||
ViewportSettings |= ViewportSettingsFlags.TransparentMouse;
|
||||
}
|
||||
|
||||
private void OnThicknessChanged (object? sender, EventArgs e)
|
||||
{
|
||||
if (!_isThicknessChanging)
|
||||
{
|
||||
_originalThickness = new (Thickness.Left, Thickness.Top, Thickness.Right, Thickness.Bottom);
|
||||
SetShadow (ShadowStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// When the Parent is drawn, we cache the clip region so we can draw the Margin after all other Views
|
||||
// QUESTION: Why can't this just be the NeedsDisplay region?
|
||||
private Region? _cachedClip;
|
||||
@@ -56,7 +59,7 @@ public class Margin : Adornment
|
||||
|
||||
internal void CacheClip ()
|
||||
{
|
||||
if (Thickness != Thickness.Empty /*&& ShadowStyle != ShadowStyle.None*/)
|
||||
if (Thickness != Thickness.Empty && ShadowStyle != ShadowStyle.None)
|
||||
{
|
||||
// PERFORMANCE: How expensive are these clones?
|
||||
_cachedClip = GetClip ()?.Clone ();
|
||||
@@ -64,12 +67,15 @@ public class Margin : Adornment
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// INTERNAL API - Draws the margins for the specified views. This is called by the <see cref="Application"/> on each
|
||||
/// INTERNAL API - Draws the transparent margins for the specified views. This is called from <see cref="View.Draw"/> on each
|
||||
/// iteration of the main loop after all Views have been drawn.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Non-transparent margins are drawn as-normal in <see cref="View.DrawAdornments"/>.
|
||||
/// </remarks>
|
||||
/// <param name="views"></param>
|
||||
/// <returns><see langword="true"/></returns>
|
||||
internal static bool DrawMargins (IEnumerable<View> views)
|
||||
internal static bool DrawTransparentMargins (IEnumerable<View> views)
|
||||
{
|
||||
Stack<View> stack = new (views);
|
||||
|
||||
@@ -77,7 +83,10 @@ public class Margin : Adornment
|
||||
{
|
||||
View view = stack.Pop ();
|
||||
|
||||
if (view.Margin is { } margin && margin.Thickness != Thickness.Empty && margin.GetCachedClip () != null)
|
||||
if (view.Margin is { } margin
|
||||
&& margin.Thickness != Thickness.Empty
|
||||
&& margin.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)
|
||||
&& margin.GetCachedClip () != null)
|
||||
{
|
||||
margin.SetNeedsDraw ();
|
||||
Region? saved = view.GetClip ();
|
||||
@@ -87,8 +96,6 @@ public class Margin : Adornment
|
||||
margin.ClearCachedClip ();
|
||||
}
|
||||
|
||||
view.ClearNeedsDraw ();
|
||||
|
||||
foreach (View subview in view.SubViews)
|
||||
{
|
||||
stack.Push (subview);
|
||||
@@ -134,7 +141,7 @@ public class Margin : Adornment
|
||||
if (ShadowStyle != ShadowStyle.None)
|
||||
{
|
||||
// Don't clear where the shadow goes
|
||||
screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT);
|
||||
screen = Rectangle.Inflate (screen, -ShadowSize.Width, -ShadowSize.Height);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -151,6 +158,8 @@ public class Margin : Adornment
|
||||
// private bool _pressed;
|
||||
private ShadowView? _bottomShadow;
|
||||
private ShadowView? _rightShadow;
|
||||
private bool _isThicknessChanging;
|
||||
private Thickness? _originalThickness;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
|
||||
@@ -172,25 +181,29 @@ public class Margin : Adornment
|
||||
_bottomShadow = null;
|
||||
}
|
||||
|
||||
_originalThickness ??= Thickness;
|
||||
|
||||
if (ShadowStyle != ShadowStyle.None)
|
||||
{
|
||||
// Turn off shadow
|
||||
Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT);
|
||||
_originalThickness = new (Thickness.Left, Thickness.Top, Math.Max (Thickness.Right - ShadowSize.Width, 0), Math.Max (Thickness.Bottom - ShadowSize.Height, 0));
|
||||
}
|
||||
|
||||
if (style != ShadowStyle.None)
|
||||
{
|
||||
// Turn on shadow
|
||||
Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT);
|
||||
_isThicknessChanging = true;
|
||||
Thickness = new (_originalThickness.Value.Left, _originalThickness.Value.Top, _originalThickness.Value.Right + ShadowSize.Width, _originalThickness.Value.Bottom + ShadowSize.Height);
|
||||
_isThicknessChanging = false;
|
||||
}
|
||||
|
||||
if (style != ShadowStyle.None)
|
||||
{
|
||||
_rightShadow = new ()
|
||||
{
|
||||
X = Pos.AnchorEnd (SHADOW_WIDTH),
|
||||
X = Pos.AnchorEnd (ShadowSize.Width),
|
||||
Y = 0,
|
||||
Width = SHADOW_WIDTH,
|
||||
Width = ShadowSize.Width,
|
||||
Height = Dim.Fill (),
|
||||
ShadowStyle = style,
|
||||
Orientation = Orientation.Vertical
|
||||
@@ -199,14 +212,20 @@ public class Margin : Adornment
|
||||
_bottomShadow = new ()
|
||||
{
|
||||
X = 0,
|
||||
Y = Pos.AnchorEnd (SHADOW_HEIGHT),
|
||||
Y = Pos.AnchorEnd (ShadowSize.Height),
|
||||
Width = Dim.Fill (),
|
||||
Height = SHADOW_HEIGHT,
|
||||
Height = ShadowSize.Height,
|
||||
ShadowStyle = style,
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
Add (_rightShadow, _bottomShadow);
|
||||
}
|
||||
else if (Thickness != _originalThickness)
|
||||
{
|
||||
_isThicknessChanging = true;
|
||||
Thickness = new (_originalThickness.Value.Left, _originalThickness.Value.Top, _originalThickness.Value.Right, _originalThickness.Value.Bottom);
|
||||
_isThicknessChanging = false;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
@@ -215,7 +234,90 @@ public class Margin : Adornment
|
||||
public override ShadowStyle ShadowStyle
|
||||
{
|
||||
get => base.ShadowStyle;
|
||||
set => base.ShadowStyle = SetShadow (value);
|
||||
set
|
||||
{
|
||||
if (value == ShadowStyle.Opaque || (value == ShadowStyle.Transparent && (ShadowSize.Width == 0 || ShadowSize.Height == 0)))
|
||||
{
|
||||
if (ShadowSize.Width != 1)
|
||||
{
|
||||
ShadowSize = ShadowSize with { Width = 1 };
|
||||
}
|
||||
|
||||
if (ShadowSize.Height != 1)
|
||||
{
|
||||
ShadowSize = ShadowSize with { Height = 1 };
|
||||
}
|
||||
}
|
||||
|
||||
base.ShadowStyle = SetShadow (value);
|
||||
}
|
||||
}
|
||||
|
||||
private Size _shadowSize;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the shadow effect.
|
||||
/// </summary>
|
||||
public Size ShadowSize
|
||||
{
|
||||
get => _shadowSize;
|
||||
set
|
||||
{
|
||||
if (TryValidateShadowSize (_shadowSize, value, out Size result))
|
||||
{
|
||||
_shadowSize = value;
|
||||
SetShadow (ShadowStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_shadowSize = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryValidateShadowSize (Size originalValue, in Size newValue, out Size result)
|
||||
{
|
||||
result = newValue;
|
||||
|
||||
bool wasValid = true;
|
||||
|
||||
if (newValue.Width < 0)
|
||||
{
|
||||
result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Width = 1 } : originalValue;
|
||||
|
||||
wasValid = false;
|
||||
}
|
||||
|
||||
|
||||
if (newValue.Height < 0)
|
||||
{
|
||||
result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Height = 1 } : originalValue;
|
||||
|
||||
wasValid = false;
|
||||
}
|
||||
|
||||
if (!wasValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasUpdated = false;
|
||||
|
||||
if ((ShadowStyle == ShadowStyle.Opaque && newValue.Width != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Width < 1))
|
||||
{
|
||||
result = result with { Width = 1 };
|
||||
|
||||
wasUpdated = true;
|
||||
}
|
||||
|
||||
if ((ShadowStyle == ShadowStyle.Opaque && newValue.Height != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Height < 1))
|
||||
{
|
||||
result = result with { Height = 1 };
|
||||
|
||||
wasUpdated = true;
|
||||
}
|
||||
|
||||
return !wasUpdated;
|
||||
}
|
||||
|
||||
private void OnParentOnMouseStateChanged (object? sender, EventArgs<MouseState> args)
|
||||
@@ -226,7 +328,7 @@ public class Margin : Adornment
|
||||
}
|
||||
|
||||
bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag (MouseState.Pressed);
|
||||
bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside); ;
|
||||
bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside);
|
||||
|
||||
if (pressedOutside)
|
||||
{
|
||||
@@ -238,11 +340,13 @@ public class Margin : Adornment
|
||||
// If the view is pressed and the highlight is being removed, move the shadow back.
|
||||
// Note, for visual effects reasons, we only move horizontally.
|
||||
// TODO: Add a setting or flag that lets the view move vertically as well.
|
||||
_isThicknessChanging = true;
|
||||
Thickness = new (
|
||||
Thickness.Left - PRESS_MOVE_HORIZONTAL,
|
||||
Thickness.Top - PRESS_MOVE_VERTICAL,
|
||||
Thickness.Right + PRESS_MOVE_HORIZONTAL,
|
||||
Thickness.Bottom + PRESS_MOVE_VERTICAL);
|
||||
_isThicknessChanging = false;
|
||||
|
||||
if (_rightShadow is { })
|
||||
{
|
||||
@@ -264,11 +368,14 @@ public class Margin : Adornment
|
||||
// If the view is not pressed, and we want highlight move the shadow
|
||||
// Note, for visual effects reasons, we only move horizontally.
|
||||
// TODO: Add a setting or flag that lets the view move vertically as well.
|
||||
_isThicknessChanging = true;
|
||||
Thickness = new (
|
||||
Thickness.Left + PRESS_MOVE_HORIZONTAL,
|
||||
Thickness.Top + PRESS_MOVE_VERTICAL,
|
||||
Thickness.Right - PRESS_MOVE_HORIZONTAL,
|
||||
Thickness.Bottom - PRESS_MOVE_VERTICAL);
|
||||
_isThicknessChanging = false;
|
||||
|
||||
MouseState |= MouseState.Pressed;
|
||||
|
||||
if (_rightShadow is { })
|
||||
|
||||
@@ -100,7 +100,13 @@ internal class ShadowView : View
|
||||
|
||||
if (c < ScreenContents?.GetLength (1) && r < ScreenContents?.GetLength (0))
|
||||
{
|
||||
AddStr (ScreenContents [r, c].Grapheme);
|
||||
string grapheme = ScreenContents [r, c].Grapheme;
|
||||
AddStr (grapheme);
|
||||
|
||||
if (grapheme.GetColumns () > 1)
|
||||
{
|
||||
c++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,21 +131,31 @@ internal class ShadowView : View
|
||||
Rectangle screen = ViewportToScreen (Viewport);
|
||||
|
||||
// Fill in the rest of the rectangle
|
||||
for (int c = Math.Max (0, screen.X); c < screen.X + screen.Width; c++)
|
||||
for (int r = Math.Max (0, screen.Y); r < screen.Y + viewport.Height; r++)
|
||||
{
|
||||
for (int r = Math.Max (0, screen.Y); r < screen.Y + viewport.Height; r++)
|
||||
for (int c = Math.Max (0, screen.X); c < screen.X + screen.Width; c++)
|
||||
{
|
||||
Driver?.Move (c, r);
|
||||
SetAttribute (GetAttributeUnderLocation (new (c, r)));
|
||||
|
||||
if (ScreenContents is { } && screen.X < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0))
|
||||
if (ScreenContents is { } && screen.X < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0)
|
||||
&& c < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0))
|
||||
{
|
||||
AddStr (ScreenContents [r, c].Grapheme);
|
||||
string grapheme = ScreenContents [r, c].Grapheme;
|
||||
AddStr (grapheme);
|
||||
|
||||
if (grapheme.GetColumns () > 1)
|
||||
{
|
||||
c++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BUGBUG: This will never really work completely right by looking at an underlying cell and trying
|
||||
// BUGBUG: to do transparency by adjusting colors. Instead, it might be possible to use the A in argb for this.
|
||||
// BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4491
|
||||
private Attribute GetAttributeUnderLocation (Point location)
|
||||
{
|
||||
if (SuperView is not Adornment
|
||||
|
||||
@@ -28,8 +28,8 @@ public partial class View // Drawing APIs
|
||||
view.Draw (context);
|
||||
}
|
||||
|
||||
// Draw the margins last to ensure they are drawn on top of the content.
|
||||
Margin.DrawMargins (viewsArray);
|
||||
// Draw Transparent margins last to ensure they are drawn on top of the content.
|
||||
Margin.DrawTransparentMargins (viewsArray);
|
||||
|
||||
// DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all.
|
||||
foreach (View view in viewsArray)
|
||||
@@ -183,7 +183,18 @@ public partial class View // Drawing APIs
|
||||
|
||||
private void DoDrawAdornmentsSubViews ()
|
||||
{
|
||||
// NOTE: We do not support SubViews of Margin
|
||||
// Only SetNeedsDraw on Margin here if it is not Transparent. Transparent Margins are drawn in a separate pass in the static View.Draw
|
||||
// via Margin.DrawTransparentMargins.
|
||||
if (Margin is { NeedsDraw: true } && !Margin.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
|
||||
{
|
||||
foreach (View subview in Margin.SubViews)
|
||||
{
|
||||
subview.SetNeedsDraw ();
|
||||
}
|
||||
|
||||
// NOTE: We do not support arbitrary SubViews of Margin (only ShadowView)
|
||||
// NOTE: so we do not call DoDrawSubViews on Margin.
|
||||
}
|
||||
|
||||
if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty && Border.NeedsDraw)
|
||||
{
|
||||
@@ -268,7 +279,12 @@ public partial class View // Drawing APIs
|
||||
/// </remarks>
|
||||
public void DrawAdornments ()
|
||||
{
|
||||
// We do not attempt to draw Margin. It is drawn in a separate pass.
|
||||
// Only draw Margin here if it is not Transparent. Transparent Margins are drawn in a separate pass in the static View.Draw
|
||||
// via Margin.DrawTransparentMargins.
|
||||
if (Margin is { } && !Margin.ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
|
||||
{
|
||||
Margin?.Draw ();
|
||||
}
|
||||
|
||||
// Each of these renders lines to this View's LineCanvas
|
||||
// Those lines will be finally rendered in OnRenderLineCanvas
|
||||
|
||||
@@ -256,7 +256,7 @@ public partial class View // Mouse APIs
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Invokes commands bound to mouse clicks via <see cref="MouseBindings"/>
|
||||
/// (default: <see cref="Command.Select"/> → <see cref="Selecting"/> event)
|
||||
/// (default: <see cref="Command.Activate"/> → <see cref="Activating"/> event)
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
@@ -295,7 +295,7 @@ public partial class View // Mouse APIs
|
||||
/// <seealso cref="MouseEvent"/>
|
||||
/// <seealso cref="OnMouseEvent"/>
|
||||
/// <seealso cref="MouseBindings"/>
|
||||
/// <seealso cref="Selecting"/>
|
||||
/// <seealso cref="Activating"/>
|
||||
/// <seealso cref="WantContinuousButtonPressed"/>
|
||||
/// <seealso cref="HighlightStates"/>
|
||||
public bool? NewMouseEvent (MouseEventArgs mouseEvent)
|
||||
@@ -414,8 +414,8 @@ public partial class View // Mouse APIs
|
||||
/// <summary>
|
||||
/// INTERNAL: For cases where the view is grabbed and the mouse is pressed, this method handles the pressed events from
|
||||
/// the driver.
|
||||
/// When <see cref="WantContinuousButtonPressed"/> is set, this method will raise the Clicked/Selecting event
|
||||
/// via <see cref="Command.Select"/> each time it is called (after the first time the mouse is pressed).
|
||||
/// When <see cref="WantContinuousButtonPressed"/> is set, this method will raise the Clicked/Activating event
|
||||
/// via <see cref="Command.Activate"/> each time it is called (after the first time the mouse is pressed).
|
||||
/// </summary>
|
||||
/// <param name="mouseEvent"></param>
|
||||
/// <returns><see langword="true"/>, if processing should stop, <see langword="false"/> otherwise.</returns>
|
||||
@@ -531,7 +531,7 @@ public partial class View // Mouse APIs
|
||||
/// <summary>
|
||||
/// INTERNAL API: Converts mouse click events into <see cref="Command"/>s by invoking the commands bound
|
||||
/// to the mouse button via <see cref="MouseBindings"/>. By default, all mouse clicks are bound to
|
||||
/// <see cref="Command.Select"/> which raises the <see cref="Selecting"/> event.
|
||||
/// <see cref="Command.Activate"/> which raises the <see cref="Activating"/> event.
|
||||
/// </summary>
|
||||
protected bool RaiseCommandsBoundToMouse (MouseEventArgs args)
|
||||
{
|
||||
|
||||
@@ -388,6 +388,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase
|
||||
);
|
||||
}
|
||||
|
||||
_popup.Visible = true;
|
||||
_popup.Move (0, 0);
|
||||
|
||||
for (var i = 0; i < toRender.Length; i++)
|
||||
|
||||
@@ -382,6 +382,7 @@
|
||||
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/ShowEditorConfigStatusBarIndicator/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/SyncToVisualStudio/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BMP/@EntryIndexedValue">BMP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CWP/@EntryIndexedValue">CWP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LL/@EntryIndexedValue">LL</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LR/@EntryIndexedValue">LR</s:String>
|
||||
@@ -416,6 +417,7 @@
|
||||
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Attribute_0020attribute/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=conhost/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Decscusr/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=diag/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gainsboro/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gonek/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -426,9 +428,11 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mazing/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ogonek/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Quattro/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=repro/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=RRGGBB/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=runnables/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=snek/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Runnables/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ungrab/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -52,13 +52,13 @@ public class ClipTests (ITestOutputHelper _output)
|
||||
Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme);
|
||||
|
||||
// When we exit Draw, the view is excluded from the clip. So drawing at 0,0, is not valid and is clipped.
|
||||
view.AddRune (0, 0, Rune.ReplacementChar);
|
||||
view.AddRune (0, 0, Glyphs.WideGlyphReplacement);
|
||||
Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme);
|
||||
|
||||
view.AddRune (-1, -1, Rune.ReplacementChar);
|
||||
view.AddRune (-1, -1, Glyphs.WideGlyphReplacement);
|
||||
Assert.Equal ("P", Application.Driver?.Contents! [1, 1].Grapheme);
|
||||
|
||||
view.AddRune (1, 1, Rune.ReplacementChar);
|
||||
view.AddRune (1, 1, Glyphs.WideGlyphReplacement);
|
||||
Assert.Equal ("P", Application.Driver?.Contents! [3, 3].Grapheme);
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ public class ClipTests (ITestOutputHelper _output)
|
||||
public void Clipping_Wide_Runes ()
|
||||
{
|
||||
Application.Driver!.SetScreenSize (30, 1);
|
||||
Application.Driver!.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
|
||||
var top = new View
|
||||
{
|
||||
@@ -231,9 +232,9 @@ public class ClipTests (ITestOutputHelper _output)
|
||||
// 012 34 56 78 90 12 34 56 78 90 12 34 56 78
|
||||
// │こ れ は 広 い ル ー ン ラ イ ン で す 。
|
||||
// 01 2345678901234 56 78 90 12 34 56
|
||||
// │<EFBFBD> |0123456989│<EFBFBD> ン ラ イ ン で す 。
|
||||
// │① |0123456989│① ン ラ イ ン で す 。
|
||||
expectedOutput = """
|
||||
│<EFBFBD>│0123456789│ ンラインです。
|
||||
│①│0123456789│ ンラインです。
|
||||
""";
|
||||
|
||||
DriverAssert.AssertDriverContentsWithFrameAre (expectedOutput, _output);
|
||||
|
||||
@@ -50,25 +50,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
|
||||
Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
|
||||
Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
|
||||
|
||||
// var s = "a\u0301\u0300\u0306";
|
||||
|
||||
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
|
||||
//ắ", output);
|
||||
|
||||
// tf.Text = "\u1eaf";
|
||||
// Application.Refresh ();
|
||||
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
|
||||
//ắ", output);
|
||||
|
||||
// tf.Text = "\u0103\u0301";
|
||||
// Application.Refresh ();
|
||||
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
|
||||
//ắ", output);
|
||||
|
||||
// tf.Text = "\u0061\u0306\u0301";
|
||||
// Application.Refresh ();
|
||||
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
|
||||
//ắ", output);
|
||||
driver.Dispose ();
|
||||
}
|
||||
|
||||
@@ -148,31 +129,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
|
||||
Assert.Equal (0, driver.Row);
|
||||
Assert.Equal (2, driver.Col);
|
||||
|
||||
//driver.AddRune ('b');
|
||||
//Assert.Equal ((Text)'b', driver.Contents [0, 1].Text);
|
||||
//Assert.Equal (0, driver.Row);
|
||||
//Assert.Equal (2, driver.Col);
|
||||
|
||||
//// Move to the last column of the first row
|
||||
//var lastCol = driver.Cols - 1;
|
||||
//driver.Move (lastCol, 0);
|
||||
//Assert.Equal (0, driver.Row);
|
||||
//Assert.Equal (lastCol, driver.Col);
|
||||
|
||||
//// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
|
||||
//driver.AddRune ('c');
|
||||
//Assert.Equal ((Text)'c', driver.Contents [0, lastCol].Text);
|
||||
//Assert.Equal (lastCol + 1, driver.Col);
|
||||
|
||||
//// Add a rune; should succeed but do nothing as it's outside of Contents
|
||||
//driver.AddRune ('d');
|
||||
//Assert.Equal (lastCol + 2, driver.Col);
|
||||
//for (var col = 0; col < driver.Cols; col++) {
|
||||
// for (var row = 0; row < driver.Rows; row++) {
|
||||
// Assert.NotEqual ((Text)'d', driver.Contents [row, col].Text);
|
||||
// }
|
||||
//}
|
||||
|
||||
driver.Dispose ();
|
||||
}
|
||||
|
||||
@@ -181,9 +137,9 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
|
||||
{
|
||||
IDriver? driver = CreateFakeDriver ();
|
||||
driver.SetScreenSize (6, 3);
|
||||
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
|
||||
driver!.Clip = new (driver.Screen);
|
||||
|
||||
driver.Clip = new (driver.Screen);
|
||||
driver.Move (1, 0);
|
||||
driver.AddStr ("┌");
|
||||
driver.Move (2, 0);
|
||||
@@ -197,14 +153,135 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
<EFBFBD>┌─┐🍎
|
||||
①┌─┐🍎
|
||||
""",
|
||||
output,
|
||||
driver);
|
||||
|
||||
driver.Refresh ();
|
||||
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m<EFBFBD>┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m①┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
output, driver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip ()
|
||||
{
|
||||
// This test verifies the fix for issue #4258
|
||||
// When a wide glyph is added and the second column is within the clip region,
|
||||
// the attribute for column N+1 should be set to match the current attribute.
|
||||
// See: OutputBufferImpl.cs line 194
|
||||
using IDriver driver = CreateFakeDriver ();
|
||||
driver.SetScreenSize (4, 2);
|
||||
|
||||
// Set a specific attribute for the wide glyph
|
||||
Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
|
||||
driver.CurrentAttribute = wideGlyphAttr;
|
||||
|
||||
// Add a wide glyph at position (0, 0)
|
||||
driver.Move (0, 0);
|
||||
driver.AddStr ("🍎");
|
||||
|
||||
// Verify the wide glyph is in column 0
|
||||
Assert.Equal ("🍎", driver.Contents! [0, 0].Grapheme);
|
||||
Assert.Equal (wideGlyphAttr, driver.Contents [0, 0].Attribute);
|
||||
|
||||
// Verify column 1 (the second column of the wide glyph) has the correct attribute set
|
||||
// This is the fix: column N+1 should have CurrentAttribute set (line 194 in OutputBufferImpl.cs)
|
||||
Assert.Equal (wideGlyphAttr, driver.Contents [0, 1].Attribute);
|
||||
|
||||
// Verify cursor moved to column 2
|
||||
Assert.Equal (2, driver.Col);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddStr_WideGlyph_Second_Column_Attribute_Not_Set_When_Outside_Clip ()
|
||||
{
|
||||
// This test verifies that when a wide glyph's second column is outside the clip,
|
||||
// the attribute for column N+1 is NOT modified
|
||||
using IDriver driver = CreateFakeDriver ();
|
||||
driver.SetScreenSize (4, 2);
|
||||
|
||||
// Set initial attribute for the entire contents
|
||||
Attribute initialAttr = new (Color.White, Color.Black);
|
||||
driver.CurrentAttribute = initialAttr;
|
||||
driver.Move (0, 0);
|
||||
driver.AddStr (" ");
|
||||
driver.Move (0, 1);
|
||||
driver.AddStr (" ");
|
||||
|
||||
// Create a clip that excludes column 1
|
||||
driver.Clip = new (new Rectangle (0, 0, 1, 2));
|
||||
|
||||
// Set a different attribute for the wide glyph
|
||||
Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
|
||||
driver.CurrentAttribute = wideGlyphAttr;
|
||||
|
||||
// Try to add a wide glyph at position (0, 0)
|
||||
// Column 0 is in clip, but column 1 is NOT
|
||||
driver.Move (0, 0);
|
||||
driver.AddStr ("🍎");
|
||||
|
||||
// Verify column 0 has the replacement character (can't fit wide glyph)
|
||||
Assert.NotEqual ("🍎", driver.Contents! [0, 0].Grapheme);
|
||||
|
||||
// Verify column 1 still has the original attribute (NOT modified)
|
||||
Assert.Equal (initialAttr, driver.Contents [0, 1].Attribute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly ()
|
||||
{
|
||||
// This test verifies the fix for issue #4258 by checking the actual driver output
|
||||
// This mimics what happens when TransparentShadow redraws a wide glyph from ScreenContents
|
||||
// WITHOUT line 194, column N+1's attribute doesn't get set, causing wrong colors in output
|
||||
// See: OutputBufferImpl.cs line ~196 (Contents [Row, Col].Attribute = CurrentAttribute;)
|
||||
using IDriver driver = CreateFakeDriver ();
|
||||
driver.SetScreenSize (3, 1);
|
||||
driver.Force16Colors = true;
|
||||
|
||||
// Step 1: Draw initial content - a wide glyph at column 1 with white-on-black
|
||||
driver.CurrentAttribute = new Attribute (Color.White, Color.Black);
|
||||
driver.Move (1, 0);
|
||||
driver.AddStr ("🍎X"); // Wide glyph at columns 1-2, 'X' at column 3 doesn't exist (off-screen)
|
||||
|
||||
// At this point:
|
||||
// - Column 0: space (default) with white-on-black
|
||||
// - Column 1: 🍎 with white-on-black
|
||||
// - Column 2: (part of 🍎) with white-on-black (from initial ClearContents)
|
||||
|
||||
// Step 2: Now redraw the SAME wide glyph at column 1 but with a DIFFERENT attribute (red-on-yellow)
|
||||
// This simulates what transparent shadow does - it redraws what's underneath with a dimmed attribute
|
||||
driver.CurrentAttribute = new Attribute (Color.BrightRed, Color.BrightYellow);
|
||||
driver.Move (1, 0);
|
||||
driver.AddStr ("🍎");
|
||||
|
||||
// Verify internal state
|
||||
Assert.Equal ("🍎", driver.Contents! [0, 1].Grapheme);
|
||||
Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 1].Attribute);
|
||||
|
||||
// THIS is the critical assertion - column 2's attribute MUST be red-on-yellow
|
||||
// WITHOUT line 194: column 2 retains white-on-black
|
||||
// WITH line 194: column 2 gets red-on-yellow
|
||||
Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 2].Attribute);
|
||||
|
||||
driver.Refresh ();
|
||||
|
||||
// Expected output:
|
||||
// Column 0: space with white-on-black
|
||||
// Columns 1-2: 🍎 with red-on-yellow (both columns must have same attribute!)
|
||||
//
|
||||
// WITHOUT line 196, the output would be:
|
||||
// \x1b[97m\x1b[40m (white-on-black for column 0)
|
||||
// \x1b[91m\x1b[103m🍎 (red-on-yellow starts at column 1)
|
||||
// \x1b[97m\x1b[40m (WRONG! Attribute changes mid-glyph because column 2 still has white-on-black)
|
||||
//
|
||||
// WITH line 196, the output is:
|
||||
// \x1b[97m\x1b[40m (white-on-black for column 0)
|
||||
// \x1b[91m\x1b[103m🍎 (red-on-yellow for both columns 1 and 2)
|
||||
DriverAssert.AssertDriverOutputIs (
|
||||
"\x1b[97m\x1b[40m \x1b[91m\x1b[103m🍎",
|
||||
output,
|
||||
driver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ public class ClipRegionTests (ITestOutputHelper output) : FakeDriverBase
|
||||
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
|
||||
|
||||
// Clear the contents
|
||||
driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), ' ');
|
||||
driver.FillRect (new (0, 0, driver.Rows, driver.Cols), new Rune(' '));
|
||||
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
|
||||
|
||||
// Setup the region with a single rectangle, fill screen with 'x'
|
||||
driver.Clip = new (new Rectangle (5, 5, 5, 5));
|
||||
driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), 'x');
|
||||
driver.Clip = new (new (5, 5, 5, 5));
|
||||
driver.FillRect (new (0, 0, driver.Rows, driver.Cols), new Rune ('x'));
|
||||
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
|
||||
Assert.Equal (" ", driver.Contents [4, 9].Grapheme);
|
||||
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Text;
|
||||
using UnitTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -104,6 +105,7 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init (driverName);
|
||||
IDriver driver = app.Driver!;
|
||||
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
|
||||
// Need to force "windows" driver to override legacy console mode for this test
|
||||
driver.IsLegacyConsole = false;
|
||||
@@ -127,14 +129,14 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
<EFBFBD>┌─┐🍎
|
||||
①┌─┐🍎
|
||||
""",
|
||||
output,
|
||||
driver);
|
||||
|
||||
driver.Refresh ();
|
||||
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m<EFBFBD>┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m①┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
output, driver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace DriverTests;
|
||||
using System.Text;
|
||||
using Terminal.Gui.Drivers;
|
||||
|
||||
namespace DriverTests;
|
||||
|
||||
public class OutputBaseTests
|
||||
{
|
||||
@@ -161,6 +164,8 @@ public class OutputBaseTests
|
||||
// FakeOutput exposes this because it's in test scope
|
||||
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
|
||||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||||
buffer.SetWideGlyphReplacement ((Rune)'①');
|
||||
|
||||
buffer.SetSize (3, 1);
|
||||
|
||||
// Write '🦮' at col 0 and 'A' at col 2
|
||||
@@ -189,9 +194,9 @@ public class OutputBaseTests
|
||||
// Column 0 was written (wide glyph)
|
||||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||||
|
||||
// Column 1 was skipped by OutputBase.Write because column 0 had a wide glyph
|
||||
// So its dirty flag remains true (it was initialized as dirty by ClearContents)
|
||||
Assert.True (buffer.Contents! [0, 1].IsDirty);
|
||||
// Column 1 was marked as clean by OutputBase.Write when it processed the wide glyph at column 0
|
||||
// See: https://github.com/gui-cs/Terminal.Gui/issues/4466
|
||||
Assert.False (buffer.Contents! [0, 1].IsDirty);
|
||||
|
||||
// Column 2 was written ('A')
|
||||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
||||
@@ -209,7 +214,7 @@ public class OutputBaseTests
|
||||
|
||||
output.Write (buffer);
|
||||
|
||||
Assert.Contains ("<EFBFBD>", output.GetLastOutput ());
|
||||
Assert.Contains ("①", output.GetLastOutput ());
|
||||
Assert.Contains ("X", output.GetLastOutput ());
|
||||
|
||||
// Dirty flags cleared for the written cells
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
using System.Text;
|
||||
|
||||
namespace DriverTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for https://github.com/gui-cs/Terminal.Gui/issues/4466.
|
||||
/// These tests validate that FillRect properly handles wide characters when overlapping existing content.
|
||||
/// Specifically, they ensure that wide characters are properly invalidated and replaced when a MessageBox border or
|
||||
/// similar UI element is drawn over them, preventing visual corruption.
|
||||
/// </summary>
|
||||
public class OutputBufferWideCharTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that FillRect properly invalidates wide characters when overwriting them.
|
||||
/// This is the core issue in #4466 - when a MessageBox border is drawn over Chinese text,
|
||||
/// the wide characters need to be properly invalidated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void FillRect_OverwritesWideChar_InvalidatesProperly ()
|
||||
{
|
||||
// Arrange - Create a buffer and draw a wide character
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw a Chinese character (2 columns wide) at position 2,1
|
||||
buffer.Move (2, 1);
|
||||
buffer.AddStr ("你"); // Chinese character "you", 2 columns wide
|
||||
|
||||
// Verify the wide character was drawn
|
||||
Assert.Equal ("你", buffer.Contents! [1, 2].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 2].IsDirty);
|
||||
|
||||
// With the fix, the second column should NOT be modified by AddStr
|
||||
// The wide glyph naturally renders across both columns
|
||||
Assert.NotEqual ("你", buffer.Contents [1, 3].Grapheme);
|
||||
|
||||
// Clear dirty flags to test FillRect behavior
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Fill a rectangle that overlaps the first column of the wide character
|
||||
// This simulates drawing a MessageBox border over Chinese text
|
||||
buffer.FillRect (new (2, 1, 1, 1), new Rune ('│'));
|
||||
|
||||
// Assert
|
||||
|
||||
// With FIXES_4466: FillRect calls AddStr, which properly invalidates the wide character
|
||||
// The wide character at [1,2] should be replaced with replacement char or the new content
|
||||
Assert.Equal ("│", buffer.Contents [1, 2].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 2].IsDirty, "Cell [1,2] should be marked dirty after FillRect");
|
||||
|
||||
// The adjacent cell should also be marked dirty for proper rendering
|
||||
Assert.True (buffer.Contents [1, 3].IsDirty, "Adjacent cell [1,3] should be marked dirty to ensure proper rendering");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that FillRect handles overwriting the second column of a wide character.
|
||||
/// When drawing at an odd column that's the second half of a wide glyph, the
|
||||
/// wide glyph should be invalidated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void FillRect_OverwritesSecondColumnOfWideChar_InvalidatesWideChar ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw a wide character at position 2,1
|
||||
buffer.Move (2, 1);
|
||||
buffer.AddStr ("好"); // Chinese character, 2 columns wide
|
||||
|
||||
Assert.Equal ("好", buffer.Contents! [1, 2].Grapheme);
|
||||
|
||||
// Clear dirty flags
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Fill at the second column of the wide character (position 3)
|
||||
buffer.FillRect (new (3, 1, 1, 1), new Rune ('│'));
|
||||
|
||||
// Assert
|
||||
// With the fix: The original wide character at col 2 should be invalidated
|
||||
// because we're overwriting its second column
|
||||
Assert.True (buffer.Contents [1, 2].IsDirty, "Wide char at col 2 should be invalidated when its second column is overwritten");
|
||||
Assert.Equal (buffer.Contents [1, 2].Grapheme, Glyphs.WideGlyphReplacement.ToString ());
|
||||
|
||||
Assert.Equal ("│", buffer.Contents [1, 3].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 3].IsDirty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the ChineseUI scenario: Drawing a MessageBox with borders over Chinese button text.
|
||||
/// This simulates the specific repro case from the issue. See: https://github.com/gui-cs/Terminal.Gui/issues/4466
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void ChineseUI_MessageBox_Over_WideChars ()
|
||||
{
|
||||
// Arrange - Simulate the ChineseUI scenario
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 10, Cols = 30,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw Chinese button text (like "你好呀")
|
||||
buffer.Move (5, 3);
|
||||
buffer.AddStr ("你好呀"); // 3 Chinese characters, 6 columns total
|
||||
|
||||
// Verify initial state
|
||||
Assert.Equal ("你", buffer.Contents! [3, 5].Grapheme);
|
||||
Assert.Equal ("好", buffer.Contents [3, 7].Grapheme);
|
||||
Assert.Equal ("呀", buffer.Contents [3, 9].Grapheme);
|
||||
|
||||
// Clear dirty flags to simulate the state before MessageBox draws
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Draw a MessageBox border that partially overlaps the Chinese text
|
||||
// This simulates the mouse moving over the border, causing HighlightState changes
|
||||
// Draw vertical line at column 8 (overlaps second char "好")
|
||||
for (var row = 2; row < 6; row++)
|
||||
{
|
||||
buffer.FillRect (new (8, row, 1, 1), new Rune ('│'));
|
||||
}
|
||||
|
||||
// Assert - The wide characters should be properly handled
|
||||
// With the fix: Wide characters are properly invalidated
|
||||
// The first character "你" at col 5 should be unaffected
|
||||
Assert.Equal ("你", buffer.Contents [3, 5].Grapheme);
|
||||
|
||||
// The second character "好" at col 7 had its second column overwritten
|
||||
// so it should be replaced with replacement char
|
||||
Assert.Equal (buffer.Contents [3, 7].Grapheme, Glyphs.WideGlyphReplacement.ToString ());
|
||||
Assert.True (buffer.Contents [3, 7].IsDirty, "Invalidated wide char should be marked dirty");
|
||||
|
||||
// The border should be drawn at col 8
|
||||
Assert.Equal ("│", buffer.Contents [3, 8].Grapheme);
|
||||
Assert.True (buffer.Contents [3, 8].IsDirty);
|
||||
|
||||
// The third character "呀" at col 9 should be unaffected
|
||||
Assert.Equal ("呀", buffer.Contents [3, 9].Grapheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that FillRect works correctly with single-width characters (baseline behavior).
|
||||
/// This should work the same with or without FIXES_4466.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void FillRect_SingleWidthChars_WorksCorrectly ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw some ASCII text
|
||||
buffer.Move (2, 1);
|
||||
buffer.AddStr ("ABC");
|
||||
|
||||
Assert.Equal ("A", buffer.Contents! [1, 2].Grapheme);
|
||||
Assert.Equal ("B", buffer.Contents [1, 3].Grapheme);
|
||||
Assert.Equal ("C", buffer.Contents [1, 4].Grapheme);
|
||||
|
||||
// Clear dirty flags
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Overwrite with FillRect
|
||||
buffer.FillRect (new (3, 1, 1, 1), new Rune ('X'));
|
||||
|
||||
// Assert - This should work the same regardless of FIXES_4466
|
||||
Assert.Equal ("A", buffer.Contents [1, 2].Grapheme);
|
||||
Assert.Equal ("X", buffer.Contents [1, 3].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 3].IsDirty);
|
||||
Assert.Equal ("C", buffer.Contents [1, 4].Grapheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests FillRect with wide characters at buffer boundaries.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void FillRect_WideChar_AtBufferBoundary ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw a wide character at the right edge (col 8, which would extend to col 9)
|
||||
buffer.Move (8, 1);
|
||||
buffer.AddStr ("山"); // Chinese character "mountain", 2 columns wide
|
||||
|
||||
Assert.Equal ("山", buffer.Contents! [1, 8].Grapheme);
|
||||
|
||||
// Clear dirty flags
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - FillRect at the wide character position
|
||||
buffer.FillRect (new (8, 1, 1, 1), new Rune ('│'));
|
||||
|
||||
// Assert
|
||||
Assert.Equal ("│", buffer.Contents [1, 8].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 8].IsDirty);
|
||||
|
||||
// Adjacent cell should be marked dirty
|
||||
Assert.True (
|
||||
buffer.Contents [1, 9].IsDirty,
|
||||
"Cell after wide char replacement should be marked dirty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests OutputBase.Write method marks cells dirty correctly for wide characters.
|
||||
/// This tests the other half of the fix in OutputBase.cs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void OutputBase_Write_WideChar_MarksCellsDirty ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 20,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw a line with wide characters
|
||||
buffer.Move (0, 1);
|
||||
buffer.AddStr ("你好"); // Two wide characters
|
||||
|
||||
// Mark all as not dirty to simulate post-Write state
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents! [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify initial state
|
||||
Assert.Equal ("你", buffer.Contents! [1, 0].Grapheme);
|
||||
Assert.Equal ("好", buffer.Contents [1, 2].Grapheme);
|
||||
|
||||
// Act - Now overwrite the first wide char by writing at its position
|
||||
buffer.Move (0, 1);
|
||||
buffer.AddStr ("A"); // Single width char
|
||||
|
||||
// Assert
|
||||
// With the fix: The first cell is replaced with 'A' and marked dirty
|
||||
Assert.Equal ("A", buffer.Contents [1, 0].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 0].IsDirty);
|
||||
|
||||
// The adjacent cell (col 1) should be marked dirty for proper rendering
|
||||
Assert.True (
|
||||
buffer.Contents [1, 1].IsDirty,
|
||||
"Adjacent cell should be marked dirty after writing single-width char over wide char");
|
||||
|
||||
// The second wide char should remain
|
||||
Assert.Equal ("好", buffer.Contents [1, 2].Grapheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that filling a rectangle with spaces properly handles wide character cleanup.
|
||||
/// This simulates clearing a region that contains wide characters.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void FillRect_WithSpaces_OverWideChars ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5, Cols = 15,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Draw a line of mixed content
|
||||
buffer.Move (2, 2);
|
||||
buffer.AddStr ("A你B好C");
|
||||
|
||||
// Verify setup
|
||||
Assert.Equal ("A", buffer.Contents! [2, 2].Grapheme);
|
||||
Assert.Equal ("你", buffer.Contents [2, 3].Grapheme);
|
||||
Assert.Equal ("B", buffer.Contents [2, 5].Grapheme);
|
||||
Assert.Equal ("好", buffer.Contents [2, 6].Grapheme);
|
||||
Assert.Equal ("C", buffer.Contents [2, 8].Grapheme);
|
||||
|
||||
// Clear dirty flags
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents [r, c].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Fill the region with spaces (simulating clearing)
|
||||
buffer.FillRect (new (3, 2, 4, 1), new Rune (' '));
|
||||
|
||||
// Assert
|
||||
// With the fix: Wide characters are properly handled
|
||||
Assert.Equal (" ", buffer.Contents [2, 3].Grapheme);
|
||||
Assert.True (buffer.Contents [2, 3].IsDirty);
|
||||
|
||||
// Wide character '你' at col 3 was replaced, so col 4 should be marked dirty
|
||||
Assert.True (
|
||||
buffer.Contents [2, 4].IsDirty,
|
||||
"Cell after replaced wide char should be dirty");
|
||||
|
||||
Assert.Equal (" ", buffer.Contents [2, 4].Grapheme);
|
||||
Assert.Equal (" ", buffer.Contents [2, 5].Grapheme);
|
||||
Assert.Equal (" ", buffer.Contents [2, 6].Grapheme);
|
||||
|
||||
// Cell 7 should be dirty because '好' was partially overwritten
|
||||
Assert.True (
|
||||
buffer.Contents [2, 7].IsDirty,
|
||||
"Adjacent cell should be dirty after wide char replacement");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the edge case where a wide character's first column is outside the clip region
|
||||
/// but the second column is inside.
|
||||
/// IMPORTANT: This test documents that the code path in WriteWideGrapheme where:
|
||||
/// - !Clip.Contains(col, row) is true (first column outside)
|
||||
/// - Clip.Contains(col + 1, row) is true (second column inside)
|
||||
/// is CURRENTLY UNREACHABLE because IsValidLocation checks Clip.Contains(col, row) and
|
||||
/// returns false before WriteWideGrapheme is called. This test verifies the current behavior
|
||||
/// (nothing is written when first column is outside clip).
|
||||
/// If the behavior should change to write the second column with a replacement character,
|
||||
/// the logic in IsValidLocation or AddGrapheme needs to be modified.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void AddStr_WideChar_FirstColumnOutsideClip_SecondColumnInside_CurrentBehavior ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5,
|
||||
Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Set custom replacement characters to verify they're being used
|
||||
Rune customColumn1Replacement = new ('◄');
|
||||
Rune customColumn2Replacement = new ('►');
|
||||
buffer.SetWideGlyphReplacement (customColumn1Replacement);
|
||||
|
||||
// Set clip region that starts at column 3 (odd column)
|
||||
// This creates a scenario where col 2 is outside clip, but col 3 is inside
|
||||
buffer.Clip = new (new (3, 1, 5, 3));
|
||||
|
||||
// Clear initial contents to ensure clean state
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents! [r, c].IsDirty = false;
|
||||
buffer.Contents [r, c].Grapheme = " ";
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Try to draw a wide character at column 2
|
||||
// Column 2 is outside clip, but column 3 is inside clip
|
||||
buffer.Move (2, 1);
|
||||
buffer.AddStr ("你"); // Chinese character "you", 2 columns wide
|
||||
|
||||
// Assert
|
||||
// CURRENT BEHAVIOR: IsValidLocation returns false when col 2 is outside clip,
|
||||
// so NOTHING is written - neither column 2 nor column 3
|
||||
Assert.Equal (" ", buffer.Contents! [1, 2].Grapheme);
|
||||
Assert.False (buffer.Contents [1, 2].IsDirty, "Cell outside clip should not be marked dirty");
|
||||
|
||||
// Column 3 is also not written because IsValidLocation returned false
|
||||
// The code path in WriteWideGrapheme that would write the replacement char
|
||||
// to column 3 is never reached
|
||||
Assert.Equal (" ", buffer.Contents [1, 3].Grapheme);
|
||||
|
||||
Assert.False (
|
||||
buffer.Contents [1, 3].IsDirty,
|
||||
"Currently, second column is not written when first column is outside clip");
|
||||
|
||||
// Verify Col has been advanced by only 1 (not by the wide character width)
|
||||
// because the grapheme was not validated/processed when IsValidLocation returned false
|
||||
Assert.Equal (3, buffer.Col);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the complementary case: wide character's second column is outside clip
|
||||
/// but first column is inside. This should use the column 1 replacement character.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void AddStr_WideChar_SecondColumnOutsideClip_FirstColumnInside_UsesColumn1Replacement ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5,
|
||||
Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Set custom replacement characters
|
||||
Rune customColumn1Replacement = new ('◄');
|
||||
Rune customColumn2Replacement = new ('►');
|
||||
buffer.SetWideGlyphReplacement (customColumn1Replacement);
|
||||
|
||||
// Set clip region that ends at column 6 (even column)
|
||||
// This creates a scenario where col 5 is inside, but col 6 is outside
|
||||
buffer.Clip = new (new (0, 1, 6, 3));
|
||||
|
||||
// Clear initial contents
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents! [r, c].IsDirty = false;
|
||||
buffer.Contents [r, c].Grapheme = " ";
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Try to draw a wide character at column 5
|
||||
// Column 5 is inside clip, but column 6 is outside clip
|
||||
buffer.Move (5, 1);
|
||||
buffer.AddStr ("好"); // Chinese character, 2 columns wide
|
||||
|
||||
// Assert
|
||||
// The first column (col 5) is inside clip but second column (6) is outside
|
||||
// Should use column 1 replacement char to indicate it can't fit
|
||||
Assert.Equal (
|
||||
customColumn1Replacement.ToString (),
|
||||
buffer.Contents! [1, 5].Grapheme);
|
||||
|
||||
Assert.True (
|
||||
buffer.Contents [1, 5].IsDirty,
|
||||
"First column should be marked dirty with replacement char when second column is clipped");
|
||||
|
||||
// The second column is outside clip boundaries entirely
|
||||
Assert.Equal (" ", buffer.Contents [1, 6].Grapheme);
|
||||
Assert.False (buffer.Contents [1, 6].IsDirty, "Cell outside clip should not be modified");
|
||||
|
||||
// Verify Col has been advanced by 2 (wide character width)
|
||||
Assert.Equal (7, buffer.Col);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when both columns of a wide character are inside the clip,
|
||||
/// the character is drawn normally without replacement characters.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[Trait ("Category", "Output")]
|
||||
public void AddStr_WideChar_BothColumnsInsideClip_DrawsNormally ()
|
||||
{
|
||||
// Arrange
|
||||
OutputBufferImpl buffer = new ()
|
||||
{
|
||||
Rows = 5,
|
||||
Cols = 10,
|
||||
CurrentAttribute = new (Color.White, Color.Black)
|
||||
};
|
||||
|
||||
// Set custom replacement characters (should NOT be used in this case)
|
||||
Rune customColumn1Replacement = new ('◄');
|
||||
Rune customColumn2Replacement = new ('►');
|
||||
buffer.SetWideGlyphReplacement (customColumn1Replacement);
|
||||
|
||||
// Set clip region that includes columns 2-7
|
||||
buffer.Clip = new (new (2, 1, 6, 3));
|
||||
|
||||
// Clear initial contents
|
||||
for (var r = 0; r < buffer.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < buffer.Cols; c++)
|
||||
{
|
||||
buffer.Contents! [r, c].IsDirty = false;
|
||||
buffer.Contents [r, c].Grapheme = " ";
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Draw a wide character at column 4 (both 4 and 5 are inside clip)
|
||||
buffer.Move (4, 1);
|
||||
buffer.AddStr ("山"); // Chinese character "mountain", 2 columns wide
|
||||
|
||||
// Assert
|
||||
// Both columns are inside clip, so the wide character should be drawn normally
|
||||
Assert.Equal ("山", buffer.Contents! [1, 4].Grapheme);
|
||||
Assert.True (buffer.Contents [1, 4].IsDirty, "First column should be marked dirty");
|
||||
|
||||
// The second column should NOT be marked dirty by WriteWideGrapheme
|
||||
// The wide glyph naturally renders across both columns without modifying column N+1
|
||||
// See: https://github.com/gui-cs/Terminal.Gui/issues/4258
|
||||
Assert.False (
|
||||
buffer.Contents [1, 5].IsDirty,
|
||||
"Adjacent cell should NOT be marked dirty when writing wide char (see #4258)");
|
||||
|
||||
// Verify no replacement characters were used
|
||||
Assert.NotEqual (customColumn1Replacement.ToString (), buffer.Contents [1, 4].Grapheme);
|
||||
Assert.NotEqual (customColumn2Replacement.ToString (), buffer.Contents [1, 5].Grapheme);
|
||||
|
||||
// Verify Col has been advanced by 2
|
||||
Assert.Equal (6, buffer.Col);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#nullable enable
|
||||
using System.Text;
|
||||
using UnitTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace ViewBaseTests.Adornments;
|
||||
|
||||
[Collection ("Global Test Setup")]
|
||||
public class BorderArrangementTests (ITestOutputHelper output)
|
||||
{
|
||||
[Fact]
|
||||
public void Arrangement_Handles_Wide_Glyphs_Correctly ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (6, 5);
|
||||
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
|
||||
|
||||
superview.Text = """
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
""";
|
||||
|
||||
View view = new ()
|
||||
{
|
||||
X = 2, Width = 4, Height = 4, BorderStyle = LineStyle.Single,
|
||||
Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
|
||||
};
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
Assert.Equal ("Absolute(2)", view.X.ToString ());
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
🍎┌──┐
|
||||
🍎│ │
|
||||
🍎│ │
|
||||
🍎└──┘
|
||||
🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
🍎◊──┐
|
||||
🍎│ │
|
||||
🍎│ │
|
||||
🍎└──↘
|
||||
🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.Equal ("Absolute(1)", view.X.ToString ());
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
<EFBFBD>◊──┐
|
||||
<EFBFBD>│ │
|
||||
<EFBFBD>│ │
|
||||
<EFBFBD>└──↘
|
||||
🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.Equal ("Absolute(0)", view.X.ToString ());
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
◊──┐🍎
|
||||
│ │🍎
|
||||
│ │🍎
|
||||
└──↘🍎
|
||||
🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Arrangement_With_SubView_In_Border_Handles_Wide_Glyphs_Correctly ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (8, 7);
|
||||
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
|
||||
|
||||
superview.Text = """
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎
|
||||
""";
|
||||
|
||||
View view = new ()
|
||||
{
|
||||
X = 2, Width = 6, Height = 6, Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
|
||||
};
|
||||
view.Border!.Thickness = new (1);
|
||||
view.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
Assert.Equal ("Absolute(2)", view.X.ToString ());
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
🍎Hi
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
🍎◊i
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎 ↘
|
||||
🍎🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.Equal ("Absolute(1)", view.X.ToString ());
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
<EFBFBD>◊i
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD>
|
||||
<EFBFBD> ↘
|
||||
🍎🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.Equal ("Absolute(0)", view.X.ToString ());
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
◊i 🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
🍎
|
||||
↘🍎
|
||||
🍎🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
}
|
||||
}
|
||||
@@ -133,4 +133,26 @@ MMM",
|
||||
Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Opaque..");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Margin_Layouts_Correctly ()
|
||||
{
|
||||
View superview = new () { Width = 10, Height = 5 };
|
||||
View view = new () { Width = 3, Height = 1, BorderStyle = LineStyle.Single };
|
||||
view.Margin!.Thickness = new (1);
|
||||
View view2 = new () { X = Pos.Right (view), Width = 3, Height = 1, BorderStyle = LineStyle.Single };
|
||||
view2.Margin!.Thickness = new (1);
|
||||
View view3 = new () { Y = Pos.Bottom (view), Width = 3, Height = 1, BorderStyle = LineStyle.Single };
|
||||
view3.Margin!.Thickness = new (1);
|
||||
superview.Add (view, view2, view3);
|
||||
|
||||
superview.LayoutSubViews ();
|
||||
|
||||
Assert.Equal (new (0, 0, 10, 5), superview.Frame);
|
||||
Assert.Equal (new (0, 0, 3, 1), view.Frame);
|
||||
Assert.Equal (Rectangle.Empty, view.Viewport);
|
||||
Assert.Equal (new (3, 0, 3, 1), view2.Frame);
|
||||
Assert.Equal (Rectangle.Empty, view2.Viewport);
|
||||
Assert.Equal (new (0, 1, 3, 1), view3.Frame);
|
||||
Assert.Equal (Rectangle.Empty, view3.Viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
namespace ViewBaseTests.Adornments;
|
||||
|
||||
[Collection ("Global Test Setup")]
|
||||
|
||||
public class ShadowStyleTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_None ()
|
||||
{
|
||||
var view = new View ();
|
||||
Assert.Equal (ShadowStyle.None, view.ShadowStyle);
|
||||
Assert.Equal (ShadowStyle.None, view.Margin!.ShadowStyle);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None)]
|
||||
[InlineData (ShadowStyle.Opaque)]
|
||||
[InlineData (ShadowStyle.Transparent)]
|
||||
public void Set_View_Sets_Margin (ShadowStyle style)
|
||||
{
|
||||
var view = new View ();
|
||||
|
||||
view.ShadowStyle = style;
|
||||
Assert.Equal (style, view.ShadowStyle);
|
||||
Assert.Equal (style, view.Margin!.ShadowStyle);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 0, 0, 0, 0)]
|
||||
[InlineData (ShadowStyle.Opaque, 0, 0, 1, 1)]
|
||||
[InlineData (ShadowStyle.Transparent, 0, 0, 1, 1)]
|
||||
public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
|
||||
{
|
||||
var superView = new View
|
||||
{
|
||||
Height = 10, Width = 10
|
||||
};
|
||||
|
||||
View view = new ()
|
||||
{
|
||||
Width = Dim.Auto (),
|
||||
Height = Dim.Auto (),
|
||||
Text = "0123",
|
||||
HighlightStates = MouseState.Pressed,
|
||||
ShadowStyle = style,
|
||||
CanFocus = true
|
||||
};
|
||||
|
||||
superView.Add (view);
|
||||
superView.BeginInit ();
|
||||
superView.EndInit ();
|
||||
|
||||
Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin!.Thickness);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 3)]
|
||||
[InlineData (ShadowStyle.Opaque, 4)]
|
||||
[InlineData (ShadowStyle.Transparent, 4)]
|
||||
public void Style_Changes_Margin_Thickness (ShadowStyle style, int expected)
|
||||
{
|
||||
var view = new View ();
|
||||
view.Margin!.Thickness = new (3);
|
||||
view.ShadowStyle = style;
|
||||
Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
|
||||
|
||||
view.ShadowStyle = ShadowStyle.None;
|
||||
Assert.Equal (new (3), view.Margin.Thickness);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
}
|
||||
487
Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
Normal file
487
Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using System.Text;
|
||||
using UnitTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace ViewBaseTests.Adornments;
|
||||
|
||||
[Collection ("Global Test Setup")]
|
||||
|
||||
public class ShadowTests (ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
|
||||
[Fact]
|
||||
public void Default_None ()
|
||||
{
|
||||
var view = new View ();
|
||||
Assert.Equal (ShadowStyle.None, view.ShadowStyle);
|
||||
Assert.Equal (ShadowStyle.None, view.Margin!.ShadowStyle);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None)]
|
||||
[InlineData (ShadowStyle.Opaque)]
|
||||
[InlineData (ShadowStyle.Transparent)]
|
||||
public void Set_View_Sets_Margin (ShadowStyle style)
|
||||
{
|
||||
var view = new View ();
|
||||
|
||||
view.ShadowStyle = style;
|
||||
Assert.Equal (style, view.ShadowStyle);
|
||||
Assert.Equal (style, view.Margin!.ShadowStyle);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 0, 0, 0, 0)]
|
||||
[InlineData (ShadowStyle.Opaque, 0, 0, 1, 1)]
|
||||
[InlineData (ShadowStyle.Transparent, 0, 0, 1, 1)]
|
||||
public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
|
||||
{
|
||||
var superView = new View
|
||||
{
|
||||
Height = 10, Width = 10
|
||||
};
|
||||
|
||||
View view = new ()
|
||||
{
|
||||
Width = Dim.Auto (),
|
||||
Height = Dim.Auto (),
|
||||
Text = "0123",
|
||||
HighlightStates = MouseState.Pressed,
|
||||
ShadowStyle = style,
|
||||
CanFocus = true
|
||||
};
|
||||
|
||||
superView.Add (view);
|
||||
superView.BeginInit ();
|
||||
superView.EndInit ();
|
||||
|
||||
Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin!.Thickness);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 3)]
|
||||
[InlineData (ShadowStyle.Opaque, 4)]
|
||||
[InlineData (ShadowStyle.Transparent, 4)]
|
||||
public void Style_Changes_Margin_Thickness (ShadowStyle style, int expected)
|
||||
{
|
||||
var view = new View ();
|
||||
view.Margin!.Thickness = new (3);
|
||||
view.ShadowStyle = style;
|
||||
Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
|
||||
|
||||
view.ShadowStyle = ShadowStyle.None;
|
||||
Assert.Equal (new (3), view.Margin.Thickness);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.Opaque)]
|
||||
[InlineData (ShadowStyle.Transparent)]
|
||||
public void ShadowWidth_ShadowHeight_Defaults_To_One (ShadowStyle style)
|
||||
{
|
||||
View view = new () { ShadowStyle = style };
|
||||
|
||||
Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 0)]
|
||||
[InlineData (ShadowStyle.Opaque, 1)]
|
||||
[InlineData (ShadowStyle.Transparent, 1)]
|
||||
public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_One (ShadowStyle style, int expectedLength)
|
||||
{
|
||||
View view = new () { ShadowStyle = style };
|
||||
view.Margin!.ShadowSize = new (-1, -1);
|
||||
Assert.Equal (expectedLength, view.Margin!.ShadowSize.Width);
|
||||
Assert.Equal (expectedLength, view.Margin!.ShadowSize.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Changing_ShadowStyle_Correctly_Set_ShadowWidth_ShadowHeight_Thickness ()
|
||||
{
|
||||
View view = new () { ShadowStyle = ShadowStyle.Transparent };
|
||||
view.Margin!.ShadowSize = new (2, 2);
|
||||
|
||||
Assert.Equal (new (2, 2), view.Margin!.ShadowSize);
|
||||
Assert.Equal (new (0, 0, 2, 2), view.Margin.Thickness);
|
||||
|
||||
view.ShadowStyle = ShadowStyle.None;
|
||||
Assert.Equal (new (2, 2), view.Margin!.ShadowSize);
|
||||
Assert.Equal (new (0, 0, 0, 0), view.Margin.Thickness);
|
||||
|
||||
view.ShadowStyle = ShadowStyle.Opaque;
|
||||
Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
|
||||
Assert.Equal (new (0, 0, 1, 1), view.Margin.Thickness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShadowStyle_Transparent_Handles_Wide_Glyphs_Correctly ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (6, 5);
|
||||
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
|
||||
|
||||
superview.Text = """
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
""";
|
||||
|
||||
View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single, ShadowStyle = ShadowStyle.Transparent };
|
||||
view.Margin!.ShadowSize = view.Margin!.ShadowSize with { Width = 2 };
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
┌──┐🍎
|
||||
│ │🍎
|
||||
│ │🍎
|
||||
└──┘🍎
|
||||
<EFBFBD> 🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
view.Margin!.ShadowSize = new (1, 2);
|
||||
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
┌──┐🍎
|
||||
│ │<EFBFBD>
|
||||
└──┘<EFBFBD>
|
||||
<EFBFBD> 🍎🍎
|
||||
<EFBFBD> 🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShadowStyle_Opaque_Change_Thickness_On_Mouse_Pressed_Released ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (10, 4);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
|
||||
View view = new () { Width = 7, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hi |", HighlightStates = MouseState.Pressed };
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
| Hi |▖
|
||||
▝▀▀▀▀▀▘
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.Button1Pressed });
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
| Hi |
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.Button1Released });
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
| Hi |▖
|
||||
▝▀▀▀▀▀▘
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShadowStyle_Transparent_Never_Throws_Navigating_Outside_Bounds ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (6, 5);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
|
||||
|
||||
superview.Text = """
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
🍎🍎🍎
|
||||
""";
|
||||
|
||||
View view = new ()
|
||||
{
|
||||
Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single, ShadowStyle = ShadowStyle.Transparent,
|
||||
Arrangement = ViewArrangement.Movable, CanFocus = true
|
||||
};
|
||||
view.Margin!.ShadowSize = view.Margin!.ShadowSize with { Width = 2 };
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
Assert.Equal (new (0, 0), view.Frame.Location);
|
||||
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
|
||||
|
||||
int i = 0;
|
||||
DecrementValue (-10, Key.CursorLeft);
|
||||
Assert.Equal (-10, i);
|
||||
|
||||
IncrementValue (0, Key.CursorRight);
|
||||
Assert.Equal (0, i);
|
||||
|
||||
DecrementValue (-10, Key.CursorUp);
|
||||
Assert.Equal (-10, i);
|
||||
|
||||
IncrementValue (20, Key.CursorDown);
|
||||
Assert.Equal (20, i);
|
||||
|
||||
DecrementValue (0, Key.CursorUp);
|
||||
Assert.Equal (0, i);
|
||||
|
||||
IncrementValue (20, Key.CursorRight);
|
||||
Assert.Equal (20, i);
|
||||
|
||||
return;
|
||||
|
||||
void DecrementValue (int count, Key key)
|
||||
{
|
||||
for (; i > count; i--)
|
||||
{
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (key));
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
CheckAssertion (new (i - 1, 0), new (0, i - 1), key);
|
||||
}
|
||||
}
|
||||
|
||||
void IncrementValue (int count, Key key)
|
||||
{
|
||||
for (; i < count; i++)
|
||||
{
|
||||
Assert.True (app.Keyboard.RaiseKeyDownEvent (key));
|
||||
app.LayoutAndDraw ();
|
||||
|
||||
CheckAssertion (new (i + 1, 0), new (0, i + 1), key);
|
||||
}
|
||||
}
|
||||
|
||||
bool? IsColumn (Key key)
|
||||
{
|
||||
if (key == Key.CursorLeft || key == Key.CursorRight)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key == Key.CursorUp || key == Key.CursorDown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void CheckAssertion (Point colLocation, Point rowLocation, Key key)
|
||||
{
|
||||
bool? isCol = IsColumn (key);
|
||||
|
||||
switch (isCol)
|
||||
{
|
||||
case true:
|
||||
Assert.Equal (colLocation, view.Frame.Location);
|
||||
|
||||
break;
|
||||
case false:
|
||||
Assert.Equal (rowLocation, view.Frame.Location);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (ShadowStyle.None, 3)]
|
||||
[InlineData (ShadowStyle.Opaque, 4)]
|
||||
[InlineData (ShadowStyle.Transparent, 4)]
|
||||
public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expected)
|
||||
{
|
||||
var view = new View ();
|
||||
view.Margin!.Thickness = new (3);
|
||||
view.ShadowStyle = style;
|
||||
Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
|
||||
|
||||
view.Margin.Thickness = new (3, 3, expected + 1, expected + 1);
|
||||
Assert.Equal (new (3, 3, expected + 1, expected + 1), view.Margin.Thickness);
|
||||
view.ShadowStyle = ShadowStyle.None;
|
||||
Assert.Equal (new (3, 3, 4, 4), view.Margin.Thickness);
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runnable_View_Overlap_Other_Runnables ()
|
||||
{
|
||||
IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
|
||||
app.Driver?.SetScreenSize (10, 5);
|
||||
|
||||
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "🍎".Repeat (25)! };
|
||||
View view = new () { Width = 7, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hi |" };
|
||||
superview.Add (view);
|
||||
|
||||
app.Begin (superview);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
| Hi |▖ 🍎
|
||||
▝▀▀▀▀▀▘ 🍎
|
||||
🍎🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎🍎
|
||||
🍎🍎🍎🍎🍎
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
Runnable modalSuperview = new () { Y = 1, Width = Dim.Fill (), Height = 4, BorderStyle = LineStyle.Single };
|
||||
View view1 = new () { Width = 8, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hey |" };
|
||||
modalSuperview.Add (view1);
|
||||
|
||||
app.Begin (modalSuperview);
|
||||
|
||||
Assert.True (modalSuperview.IsModal);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
| Hi |▖ 🍎
|
||||
┌────────┐
|
||||
│| Hey |▖│
|
||||
│▝▀▀▀▀▀▀▘│
|
||||
└────────┘
|
||||
""",
|
||||
output,
|
||||
app.Driver);
|
||||
|
||||
|
||||
app.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransparentShadow_Draws_Transparent_At_Driver_Output ()
|
||||
{
|
||||
// Arrange
|
||||
using IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
app.Driver!.SetScreenSize (2, 1);
|
||||
app.Driver.Force16Colors = true;
|
||||
|
||||
using Runnable superView = new ();
|
||||
superView.Width = Dim.Fill ();
|
||||
superView.Height = Dim.Fill ();
|
||||
superView.Text = "AB";
|
||||
superView.TextFormatter.WordWrap = true;
|
||||
superView.SetScheme (new (new Attribute (Color.Black, Color.White)));
|
||||
|
||||
// Create view with transparent shadow
|
||||
View viewWithShadow = new ()
|
||||
{
|
||||
Width = Dim.Auto (),
|
||||
Height = Dim.Auto (),
|
||||
Text = "*",
|
||||
ShadowStyle = ShadowStyle.Transparent
|
||||
};
|
||||
// Make it so the margin is only on the right for simplicity
|
||||
viewWithShadow.Margin!.Thickness = new (0, 0, 1, 0);
|
||||
viewWithShadow.SetScheme (new (new Attribute (Color.Black, Color.White)));
|
||||
|
||||
superView.Add (viewWithShadow);
|
||||
|
||||
// Act
|
||||
app.Begin (superView);
|
||||
app.LayoutAndDraw ();
|
||||
app.Driver.Refresh ();
|
||||
|
||||
// Assert
|
||||
_output.WriteLine ("Actual driver contents:");
|
||||
_output.WriteLine (app.Driver.ToString ());
|
||||
_output.WriteLine ("\nActual driver output:");
|
||||
string? output = app.Driver.GetOutput ().GetLastOutput ();
|
||||
_output.WriteLine (output);
|
||||
|
||||
DriverAssert.AssertDriverOutputIs ("""
|
||||
\x1b[30m\x1b[107m*\x1b[90m\x1b[100mB
|
||||
""", _output, app.Driver);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransparentShadow_OverWide_Draws_Transparent_At_Driver_Output ()
|
||||
{
|
||||
// Arrange
|
||||
using IApplication app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
app.Driver!.SetScreenSize (2, 3);
|
||||
app.Driver.Force16Colors = true;
|
||||
|
||||
using Runnable superView = new ();
|
||||
superView.Width = Dim.Fill ();
|
||||
superView.Height = Dim.Fill ();
|
||||
superView.Text = "🍎🍎🍎🍎";
|
||||
superView.TextFormatter.WordWrap = true;
|
||||
superView.SetScheme (new (new Attribute (Color.Black, Color.White)));
|
||||
|
||||
// Create view with transparent shadow
|
||||
View viewWithShadow = new ()
|
||||
{
|
||||
Width = Dim.Auto (),
|
||||
Height = Dim.Auto (),
|
||||
Text = "*",
|
||||
ShadowStyle = ShadowStyle.Transparent
|
||||
};
|
||||
// Make it so the margin is only on the bottom for simplicity
|
||||
viewWithShadow.Margin!.Thickness = new (0, 0, 0, 1);
|
||||
viewWithShadow.SetScheme (new (new Attribute (Color.Black, Color.White)));
|
||||
|
||||
superView.Add (viewWithShadow);
|
||||
|
||||
// Act
|
||||
app.Begin (superView);
|
||||
app.LayoutAndDraw ();
|
||||
app.Driver.Refresh ();
|
||||
|
||||
// Assert
|
||||
_output.WriteLine ("Actual driver contents:");
|
||||
_output.WriteLine (app.Driver.ToString ());
|
||||
_output.WriteLine ("\nActual driver output:");
|
||||
string? output = app.Driver.GetOutput ().GetLastOutput ();
|
||||
_output.WriteLine (output);
|
||||
|
||||
DriverAssert.AssertDriverOutputIs ("""
|
||||
\x1b[30m\x1b[107m*\x1b[90m\x1b[103m \x1b[97m\x1b[40m \x1b[90m\x1b[100m \x1b[97m\x1b[40m🍎
|
||||
""", _output, app.Driver);
|
||||
}
|
||||
}
|
||||
@@ -574,6 +574,7 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
};
|
||||
|
||||
superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2);
|
||||
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
app.Begin (superView);
|
||||
// Begin calls LayoutAndDraw, so no need to call it again here
|
||||
// app.LayoutAndDraw();
|
||||
@@ -585,9 +586,9 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
┆viewWithBorderAtX0┆🍎🍎🍎
|
||||
└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
|
||||
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
|
||||
<EFBFBD>┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎
|
||||
<EFBFBD>┆viewWithBorderAtX1┆ 🍎🍎
|
||||
<EFBFBD>└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎
|
||||
①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎
|
||||
①┆viewWithBorderAtX1┆ 🍎🍎
|
||||
①└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎
|
||||
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
|
||||
🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
|
||||
🍎┆viewWithBorderAtX2┆🍎🍎
|
||||
@@ -597,7 +598,7 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
output,
|
||||
driver);
|
||||
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<EFBFBD>┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<EFBFBD>┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<EFBFBD>└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
|
||||
output, driver);
|
||||
|
||||
DriverImpl? driverImpl = driver as DriverImpl;
|
||||
@@ -617,9 +618,9 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
┆viewWithBorderAtX0┆🍎🍎🍎
|
||||
└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎
|
||||
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
|
||||
<EFBFBD>┌──────────────────┐ 🍎🍎
|
||||
<EFBFBD>│viewWithBorderAtX1│ 🍎🍎
|
||||
<EFBFBD>└──────────────────┘ 🍎🍎
|
||||
①┌──────────────────┐ 🍎🍎
|
||||
①│viewWithBorderAtX1│ 🍎🍎
|
||||
①└──────────────────┘ 🍎🍎
|
||||
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
|
||||
🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎
|
||||
🍎┆viewWithBorderAtX2┆🍎🍎
|
||||
@@ -675,18 +676,19 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
};
|
||||
|
||||
superView.Add (viewWithBorder);
|
||||
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
app.Begin (superView);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
<EFBFBD>┌─┐🍎
|
||||
<EFBFBD>│X│🍎
|
||||
<EFBFBD>└─┘🍎
|
||||
①┌─┐🍎
|
||||
①│X│🍎
|
||||
①└─┘🍎
|
||||
""",
|
||||
output,
|
||||
driver);
|
||||
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<EFBFBD>┌─┐🍎<EFBFBD>│X│🍎<EFBFBD>└─┘🍎",
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┌─┐🍎①│X│🍎①└─┘🍎",
|
||||
output, driver);
|
||||
|
||||
DriverImpl? driverImpl = driver as DriverImpl;
|
||||
@@ -738,19 +740,21 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
|
||||
Height = 3
|
||||
};
|
||||
|
||||
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
|
||||
|
||||
superView.Add (viewWithBorder);
|
||||
app.Begin (superView);
|
||||
|
||||
DriverAssert.AssertDriverContentsAre (
|
||||
"""
|
||||
🍎<EFBFBD>┌─┐
|
||||
🍎<EFBFBD>│X│
|
||||
🍎<EFBFBD>└─┘
|
||||
🍎①┌─┐
|
||||
🍎①│X│
|
||||
🍎①└─┘
|
||||
""",
|
||||
output,
|
||||
driver);
|
||||
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎<EFBFBD>┌─┐🍎<EFBFBD>│X│🍎<EFBFBD>└─┘",
|
||||
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎①┌─┐🍎①│X│🍎①└─┘",
|
||||
output, driver);
|
||||
|
||||
DriverImpl? driverImpl = driver as DriverImpl;
|
||||
|
||||
Reference in New Issue
Block a user