diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 74586e878..cc7b5009f 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -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", diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 6dd491f65..453ee2478 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -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. diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs index 907cbb42e..f4339b801 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs @@ -1,6 +1,4 @@ #nullable enable -using System; - namespace UICatalog.Scenarios; /// @@ -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> 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) + }) + ; }; } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs index 30c3ffda4..b65169f5d 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentsEditor.cs @@ -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 /// 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); diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs index f8f78ac6f..b495cc200 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs @@ -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 (out _)) + if (!view.Width.Has (out _)) { view.Width = Dim.Fill (); } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs index ca962eb30..9f1f799cc 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs @@ -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 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 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 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; } } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs index 7f1f795c9..b8e28730c 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/DimEditor.cs @@ -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; - /// + /// 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 selected) { DimChanged (); } - // These need to have same order - private readonly List _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 _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) { diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs index b4c548d0c..9556be4a2 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EditorBase.cs @@ -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 /// 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; } } + + /// + protected override void Dispose (bool disposing) + { + if (disposing && App is {}) + { + App.Navigation!.FocusedChanged -= NavigationOnFocusedChanged; + App.Mouse.MouseEvent -= ApplicationOnMouseEvent; + } + + base.Dispose (disposing); + } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs index 4bdd61066..25acdb876 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/EventLog.cs @@ -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) { diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs index 13c2aee8a..2630703fa 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs @@ -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 /// True of the event was cancelled. protected virtual bool OnOrientationChanging (Orientation newOrientation) { - CancelEventArgs args = new CancelEventArgs (in _orientation, ref newOrientation); + CancelEventArgs 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; } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/LayoutEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/LayoutEditor.cs index cb84ac31d..a0034ee2a 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/LayoutEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/LayoutEditor.cs @@ -1,8 +1,4 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; - namespace UICatalog.Scenarios; /// @@ -64,7 +60,6 @@ public class LayoutEditor : EditorBase X = Pos.Right (_xEditor) + 1 }; - _widthEditor = new () { Title = "_Width", diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs index 7d5d0f254..2904171d8 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs @@ -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 () + _flagSelectorTransparent = new FlagSelector { 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!; }; } -} \ No newline at end of file +} diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs index 467b54756..ae35161e6 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/PosEditor.cs @@ -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 selected) { PosChanged (); } - // These need to have same order + // These need to have same order private readonly List _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), diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs index cfca9f433..5b6679bbf 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewPropertiesEditor.cs @@ -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; } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs index e54835989..b7a86feaf 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs @@ -1,6 +1,4 @@ #nullable enable -using System; - namespace UICatalog.Scenarios; /// @@ -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 e) + void AllowNegativeXToggle (object? sender, ResultEventArgs 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 e) + void AllowXGreaterThanContentWidthToggle (object? sender, ResultEventArgs 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 e) + void AllowNegativeYToggle (object? sender, ResultEventArgs 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 e) + void AllowYGreaterThanContentHeightToggle (object? sender, ResultEventArgs 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 e) + void ContentSizeWidthValueChanged (object? sender, CancelEventArgs 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 e) + void ContentSizeHeightValueChanged (object? sender, CancelEventArgs 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 e) + void ClearContentOnlyToggle (object? sender, ResultEventArgs 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 e) + void ClipContentOnlyToggle (object? sender, ResultEventArgs 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 e) + void TransparentToggle (object? sender, ResultEventArgs 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 e) + void TransparentMouseToggle (object? sender, ResultEventArgs 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 e) + void VerticalScrollBarToggle (object? sender, ResultEventArgs 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 e) + void AutoShowVerticalScrollBarToggle (object? sender, ResultEventArgs 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 e) + void HorizontalScrollBarToggle (object? sender, ResultEventArgs 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 e) + void AutoShowHorizontalScrollBarToggle (object? sender, ResultEventArgs rea) { - ViewToEdit!.HorizontalScrollBar.AutoShow = e.Result == CheckState.Checked; + ViewToEdit!.HorizontalScrollBar.AutoShow = rea.Result == CheckState.Checked; } Add ( diff --git a/Examples/UICatalog/Scenarios/ShadowStyles.cs b/Examples/UICatalog/Scenarios/ShadowStyles.cs index 25a2a335c..2ca3002d5 100644 --- a/Examples/UICatalog/Scenarios/ShadowStyles.cs +++ b/Examples/UICatalog/Scenarios/ShadowStyles.cs @@ -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; + } } diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index b4934a064..4a3f66c4f 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -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); diff --git a/Examples/UICatalog/Scenarios/WideGlyphs.cs b/Examples/UICatalog/Scenarios/WideGlyphs.cs index 16e30b723..7d95dec34 100644 --- a/Examples/UICatalog/Scenarios/WideGlyphs.cs +++ b/Examples/UICatalog/Scenarios/WideGlyphs.cs @@ -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; diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 9f8e0a9b8..8926f2423 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -36,7 +36,7 @@ internal partial class ApplicationImpl public event EventHandler>? Iteration; /// - public void RaiseIteration () { Iteration?.Invoke (null, new (this)); } + public void RaiseIteration () { Iteration?.Invoke (this, new (this)); } #endregion Main Loop Iteration diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index 871bd3691..a852f642e 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -89,7 +89,7 @@ public class ApplicationNavigation _focused = value; - FocusedChanged?.Invoke (null, EventArgs.Empty); + FocusedChanged?.Invoke (this, EventArgs.Empty); } /// diff --git a/Terminal.Gui/App/CWP/CWPEventHelper.cs b/Terminal.Gui/App/CWP/CWPEventHelper.cs index 4840a358c..72fd9f7b5 100644 --- a/Terminal.Gui/App/CWP/CWPEventHelper.cs +++ b/Terminal.Gui/App/CWP/CWPEventHelper.cs @@ -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; } diff --git a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs index 09bcd7fa0..d7095fd7e 100644 --- a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs +++ b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs @@ -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 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; diff --git a/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs index 4c0328589..061df53d9 100644 --- a/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs +++ b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs @@ -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) diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index dbdd2d67c..40041c974 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -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) { diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 59b5d16f9..355c72879 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -86,7 +86,7 @@ internal class MouseImpl : IMouse, IDisposable mouseEvent.View = deepestViewUnderMouse; } - MouseEvent?.Invoke (null, mouseEvent); + MouseEvent?.Invoke (this, mouseEvent); if (mouseEvent.Handled) { diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index 71336009d..f83666006 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -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 + /// 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). + [ConfigurationProperty (Scope = typeof (ThemeScope))] + public static Rune WideGlyphReplacement { get; set; } = (Rune)' '; + /// File icon. Defaults to ☰ (Trigram For Heaven) [ConfigurationProperty (Scope = typeof (ThemeScope))] public static Rune File { get; set; } = (Rune)'☰'; diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 5c0d5ad72..ed61da17a 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -146,6 +146,9 @@ internal class DriverImpl : IDriver private readonly IOutput _output; + /// + public IOutputBuffer GetOutputBuffer () => OutputBuffer; + public IOutput GetOutput () => _output; private readonly IInputProcessor _inputProcessor; @@ -330,9 +333,6 @@ internal class DriverImpl : IDriver /// public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); } - /// - public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); } - /// public Attribute SetAttribute (Attribute newAttribute) { diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 0abd121b7..eda8841c8 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -64,7 +64,13 @@ public interface IDriver : IDisposable IInputProcessor GetInputProcessor (); /// - /// Gets the output handler responsible for writing to the terminal. + /// Gets the containing the buffered screen contents. + /// + /// + IOutputBuffer GetOutputBuffer (); + + /// + /// Gets the responsible for writing to the terminal. /// IOutput GetOutput (); @@ -257,14 +263,6 @@ public interface IDriver : IDisposable /// The Rune used to fill the rectangle void FillRect (Rectangle rect, Rune rune = default); - /// - /// Fills the specified rectangle with the specified . This method is a convenience method - /// that calls . - /// - /// - /// - void FillRect (Rectangle rect, char c); - /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. /// Implementations should call base.SetAttribute(c). /// C. diff --git a/Terminal.Gui/Drivers/IOutputBuffer.cs b/Terminal.Gui/Drivers/IOutputBuffer.cs index 3344d0ba8..cf096b834 100644 --- a/Terminal.Gui/Drivers/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/IOutputBuffer.cs @@ -1,5 +1,4 @@ - -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// 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. /// int Top { get; set; } + + /// + /// 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 . + /// + /// + /// 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). + /// + void SetWideGlyphReplacement (Rune column1ReplacementChar); } diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 347eba70b..cbeb403a0 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -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; + } } } diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs index c12dc29f5..00592f0e9 100644 --- a/Terminal.Gui/Drivers/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs @@ -65,6 +65,14 @@ public class OutputBufferImpl : IOutputBuffer /// The topmost row in the terminal. public virtual int Top { get; set; } = 0; + private Rune _column1ReplacementChar = Glyphs.WideGlyphReplacement; + + /// + public void SetWideGlyphReplacement (Rune column1ReplacementChar) + { + _column1ReplacementChar = column1ReplacementChar; + } + /// /// Indicates which lines have been modified and need to be redrawn. /// @@ -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 /// /// /// When the method returns, will be incremented by the number of columns - /// required, even if the new column value is outside of the or screen + /// required, even if the new column value is outside the or screen /// dimensions defined by . /// /// @@ -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++; } } } /// - /// 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. + /// + /// The column. + /// The row. + private void SetAttributeAndDirty (int col, int row) + { + Contents! [row, col].Attribute = CurrentAttribute; + Contents [row, col].IsDirty = true; + } + + /// + /// 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. /// - private void InvalidateOverlappedWideGlyph () + /// The column. + /// The row. + 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; } } /// - /// 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). /// + /// The column. + /// The row. /// The printable text to write. /// The column width of the text. /// The clipping rectangle. - 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; } } /// - /// Writes a single-width character (0 or 1 column wide). + /// INTERNAL: Writes a (0 or 1 column wide) Grapheme. /// - private void WriteSingleWidthGrapheme (string text, Rectangle clipRect) + /// The column. + /// The row. + /// The single-width Grapheme to write. + /// The clipping rectangle. + 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; } } /// - /// 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. /// - private void WriteWideGrapheme (string text) + /// The column. + /// The row. + /// The wide Grapheme to write. + 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); } /// Tests whether the specified coordinate are valid for drawing the specified Text. @@ -342,8 +362,9 @@ public class OutputBufferImpl : IOutputBuffer /// 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 /// /// Updates and to the specified column and row in . /// Used by and to determine where to add content. @@ -393,9 +414,8 @@ public class OutputBufferImpl : IOutputBuffer /// /// Column to move to. /// Row to move to. - 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; } diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 54e9c2a67..998478b15 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -1,8 +1,3 @@ - - -using System.Diagnostics; -using System.Runtime.InteropServices; - namespace Terminal.Gui.ViewBase; /// The Margin for a . Accessed via @@ -21,8 +16,6 @@ namespace Terminal.Gui.ViewBase; /// 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 } /// - /// INTERNAL API - Draws the margins for the specified views. This is called by the on each + /// INTERNAL API - Draws the transparent margins for the specified views. This is called from on each /// iteration of the main loop after all Views have been drawn. /// + /// + /// Non-transparent margins are drawn as-normal in . + /// /// /// - internal static bool DrawMargins (IEnumerable views) + internal static bool DrawTransparentMargins (IEnumerable views) { Stack 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; /// /// 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; + + /// + /// Gets or sets the size of the shadow effect. + /// + 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 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 { }) diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs index 90f84219c..78eb31355 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -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 diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index a5e17da18..38d398c3f 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -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 /// 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 diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index a1df6dd59..88cc4579d 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -256,7 +256,7 @@ public partial class View // Mouse APIs /// /// /// Invokes commands bound to mouse clicks via - /// (default: event) + /// (default: event) /// /// /// @@ -295,7 +295,7 @@ public partial class View // Mouse APIs /// /// /// - /// + /// /// /// public bool? NewMouseEvent (MouseEventArgs mouseEvent) @@ -414,8 +414,8 @@ public partial class View // Mouse APIs /// /// INTERNAL: For cases where the view is grabbed and the mouse is pressed, this method handles the pressed events from /// the driver. - /// When is set, this method will raise the Clicked/Selecting event - /// via each time it is called (after the first time the mouse is pressed). + /// When is set, this method will raise the Clicked/Activating event + /// via each time it is called (after the first time the mouse is pressed). /// /// /// , if processing should stop, otherwise. @@ -531,7 +531,7 @@ public partial class View // Mouse APIs /// /// INTERNAL API: Converts mouse click events into s by invoking the commands bound /// to the mouse button via . By default, all mouse clicks are bound to - /// which raises the event. + /// which raises the event. /// protected bool RaiseCommandsBoundToMouse (MouseEventArgs args) { diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 8a7ca9d3e..996abc68f 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -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++) diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index a33c71b8d..dfbd047be 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -382,6 +382,7 @@ False True True + BMP CWP LL LR @@ -416,6 +417,7 @@ True True True + True True True @@ -426,9 +428,11 @@ True True True + True True True True + True True True True diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs index 565795f85..0210aa742 100644 --- a/Tests/UnitTests/View/Draw/ClipTests.cs +++ b/Tests/UnitTests/View/Draw/ClipTests.cs @@ -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 - // │� |0123456989│� ン ラ イ ン で す 。 + // │① |0123456989│① ン ラ イ ン で す 。 expectedOutput = """ - │�│0123456789│ ンラインです。 + │①│0123456789│ ンラインです。 """; DriverAssert.AssertDriverContentsWithFrameAre (expectedOutput, _output); diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs index 4ec35f770..c1c4f93e6 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs @@ -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 ( """ - �┌─┐🍎 + ①┌─┐🍎 """, output, driver); driver.Refresh (); - 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", + 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); + } } diff --git a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs index c07d4070d..d36323999 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs @@ -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); diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 9d88eb730..371331deb 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -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 ( """ - �┌─┐🍎 + ①┌─┐🍎 """, output, driver); driver.Refresh (); - 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", + 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); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index 6ac00a739..9931551b0 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -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 ("�", output.GetLastOutput ()); + Assert.Contains ("①", output.GetLastOutput ()); Assert.Contains ("X", output.GetLastOutput ()); // Dirty flags cleared for the written cells diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs new file mode 100644 index 000000000..4e6a596af --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs @@ -0,0 +1,544 @@ +using System.Text; + +namespace DriverTests; + +/// +/// 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. +/// +public class OutputBufferWideCharTests +{ + /// + /// 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. + /// + [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"); + } + + /// + /// 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. + /// + [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); + } + + /// + /// 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 + /// + [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); + } + + /// + /// Tests that FillRect works correctly with single-width characters (baseline behavior). + /// This should work the same with or without FIXES_4466. + /// + [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); + } + + /// + /// Tests FillRect with wide characters at buffer boundaries. + /// + [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"); + } + + /// + /// Tests OutputBase.Write method marks cells dirty correctly for wide characters. + /// This tests the other half of the fix in OutputBase.cs. + /// + [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); + } + + /// + /// Tests that filling a rectangle with spaces properly handles wide character cleanup. + /// This simulates clearing a region that contains wide characters. + /// + [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"); + } + + /// + /// 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. + /// + [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); + } + + /// + /// 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. + /// + [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); + } + + /// + /// Tests that when both columns of a wide character are inside the clip, + /// the character is drawn normally without replacement characters. + /// + [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); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs index e69de29bb..71ad1202c 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs @@ -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 ( + """ + �◊──┐ + �│ │ + �│ │ + �└──↘ + 🍎🍎🍎 + """, + 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 ( + """ + �◊i + � + � + � + � + � ↘ + 🍎🍎🍎🍎 + """, + 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); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs index 482b2519e..eeaf1165a 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs @@ -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); + } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs deleted file mode 100644 index 49bb7f0e7..000000000 --- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs +++ /dev/null @@ -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 (); - } - -} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs new file mode 100644 index 000000000..281fa1b2f --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs @@ -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 ( + """ + ┌──┐🍎 + │ │🍎 + │ │🍎 + └──┘🍎 + � 🍎🍎 + """, + output, + app.Driver); + + view.Margin!.ShadowSize = new (1, 2); + + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsAre ( + """ + ┌──┐🍎 + │ │� + └──┘� + � 🍎🍎 + � 🍎🍎 + """, + 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); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs index 7774d1886..e24280295 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs @@ -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┆🍎🍎🍎 └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - �┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎 - �┆viewWithBorderAtX1┆ 🍎🍎 - �└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎 + ①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎 + ①┆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�┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\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", + 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┆🍎🍎🍎 └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - �┌──────────────────┐ 🍎🍎 - �│viewWithBorderAtX1│ 🍎🍎 - �└──────────────────┘ 🍎🍎 + ①┌──────────────────┐ 🍎🍎 + ①│viewWithBorderAtX1│ 🍎🍎 + ①└──────────────────┘ 🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎 🍎┆viewWithBorderAtX2┆🍎🍎 @@ -675,18 +676,19 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas }; superView.Add (viewWithBorder); + driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); app.Begin (superView); DriverAssert.AssertDriverContentsAre ( """ - �┌─┐🍎 - �│X│🍎 - �└─┘🍎 + ①┌─┐🍎 + ①│X│🍎 + ①└─┘🍎 """, output, driver); - DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┌─┐🍎�│X│🍎�└─┘🍎", + 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 ( """ - 🍎�┌─┐ - 🍎�│X│ - 🍎�└─┘ + 🍎①┌─┐ + 🍎①│X│ + 🍎①└─┘ """, output, driver); - DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎�┌─┐🍎�│X│🍎�└─┘", + DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎①┌─┐🍎①│X│🍎①└─┘", output, driver); DriverImpl? driverImpl = driver as DriverImpl;