浏览代码

Addresses #4058. Basic support for non-color text styles. (#4071)

* TextStyle enum

* CSI_AppendTextStyleChange

* Add TextStyle to Attribute

* Apply text style in NetOutput.Write()

* Don't append escape code if nothing to change

* Make TextStyle an init property

* Apply TextStyle to OutputBuffer attributes

* Fix flag checking

Misunderstood how Enum.HasFlag worked, fixed now

* Allow bold-faint text

Also adds remarks to TextStyle noting that they may be incompatible depending on terminal settings.

* Remove unnecessary check

Realized it's actually impossible for no escape codes to be added, as this is only the case when prev and next are the same, which is already accounted for.

* Remove redundant check

Attributes are records, and thus already use equality-by-value, meaning attr != redrawAttr will already be false when the TextStyle changes.

* WindowsOutput support for text style

---------

Co-authored-by: Tig <[email protected]>
Error-String-Expected-Got-Nil 4 月之前
父节点
当前提交
40602ee628

+ 130 - 0
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -1849,6 +1849,136 @@ public static class EscSeqUtils
 
     #endregion
 
+    #region Text Styles
+
+    /// <summary>
+    /// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one <see cref="TextStyle"/> to another.
+    /// </summary>
+    /// <param name="output"><see cref="StringBuilder"/> to add escape sequence to.</param>
+    /// <param name="prev">Previous <see cref="TextStyle"/> to change away from.</param>
+    /// <param name="next">Next <see cref="TextStyle"/> to change to.</param>
+    /// <remarks>
+    /// <para>
+    /// Unlike colors, most text styling options are not mutually exclusive with each other, and can be applied independently. This creates a problem when
+    /// switching from one style to another: For instance, if your previous style is just bold, and your next style is just italic, then simply adding the
+    /// sequence to enable italic text would cause the text to remain bold. This method automatically handles this problem, enabling and disabling styles as
+    /// necessary to apply exactly the next style.
+    /// </para>
+    /// </remarks>
+    internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle prev, TextStyle next)
+    {
+        // Do nothing if styles are the same, as no changes are necessary.
+        if (prev == next)
+        {
+            return;
+        }
+
+        // Bitwise operations to determine flag changes. A ^ B are the flags different between two flag sets. These different flags that exist in the next flag
+        // set (diff & next) are the ones that were enabled in the switch, those that exist in the previous flag set (diff & prev) are the ones that were
+        // disabled.
+        var diff = prev ^ next;
+        var enabled = diff & next;
+        var disabled = diff & prev;
+
+        // List of escape codes to apply.
+        var sgr = new List<int> ();
+
+        if (disabled != TextStyle.None)
+        {
+            // Special case: Both bold and faint have the same disabling code. While unusual, it can be valid to have both enabled at the same time, so when
+            // one and only one of them is being disabled, we need to re-enable the other afterward. We can check what flags remain enabled by taking
+            // prev & next, as this is the set of flags both have.
+            if (disabled.HasFlag (TextStyle.Bold))
+            {
+                sgr.Add (22);
+
+                if ((prev & next).HasFlag (TextStyle.Faint))
+                {
+                    sgr.Add (2);
+                }
+            }
+
+            if (disabled.HasFlag (TextStyle.Faint))
+            {
+                sgr.Add (22);
+
+                if ((prev & next).HasFlag (TextStyle.Bold))
+                {
+                    sgr.Add (1);
+                }
+            }
+
+            if (disabled.HasFlag (TextStyle.Italic))
+            {
+                sgr.Add (23);
+            }
+
+            if (disabled.HasFlag (TextStyle.Underline))
+            {
+                sgr.Add (24);
+            }
+
+            if (disabled.HasFlag (TextStyle.Blink))
+            {
+                sgr.Add (25);
+            }
+
+            if (disabled.HasFlag (TextStyle.Reverse))
+            {
+                sgr.Add (27);
+            }
+
+            if (disabled.HasFlag (TextStyle.Strikethrough))
+            {
+                sgr.Add (29);
+            }
+        }
+
+        if (enabled != TextStyle.None)
+        {
+            if (enabled.HasFlag (TextStyle.Bold))
+            {
+                sgr.Add (1);
+            }
+
+            if (enabled.HasFlag (TextStyle.Faint))
+            {
+                sgr.Add (2);
+            }
+
+            if (enabled.HasFlag (TextStyle.Italic))
+            {
+                sgr.Add (3);
+            }
+
+            if (enabled.HasFlag (TextStyle.Underline))
+            {
+                sgr.Add (4);
+            }
+
+            if (enabled.HasFlag (TextStyle.Blink))
+            {
+                sgr.Add (5);
+            }
+
+            if (enabled.HasFlag (TextStyle.Reverse))
+            {
+                sgr.Add (7);
+            }
+
+            if (enabled.HasFlag (TextStyle.Strikethrough))
+            {
+                sgr.Add (9);
+            }
+        }
+
+        output.Append ("\x1b[");
+        output.Append (string.Join (';', sgr));
+        output.Append ('m');
+    }
+
+    #endregion Text Styles
+
     #region Requests
 
     /// <summary>

+ 7 - 0
Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs

@@ -12,6 +12,9 @@ public class NetOutput : IConsoleOutput
 
     private CursorVisibility? _cachedCursorVisibility;
 
+    // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
+    private TextStyle _redrawTextStyle = TextStyle.None;
+
     /// <summary>
     ///     Creates a new instance of the <see cref="NetOutput"/> class.
     /// </summary>
@@ -134,6 +137,10 @@ public class NetOutput : IConsoleOutput
                             attr.Background.G,
                             attr.Background.B
                         );
+
+                        EscSeqUtils.CSI_AppendTextStyleChange (output, _redrawTextStyle, attr.TextStyle);
+
+                        _redrawTextStyle = attr.TextStyle;
                     }
 
                     outputWidth++;

+ 2 - 1
Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs

@@ -33,7 +33,8 @@ public class OutputBuffer : IOutputBuffer
             // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
             if (Application.Driver is { })
             {
-                _currentAttribute = new (value.Foreground, value.Background);
+                // TODO: Update this when attributes can include TextStyle in the constructor
+                _currentAttribute = new (value.Foreground, value.Background) { TextStyle = value.TextStyle };
 
                 return;
             }

+ 5 - 0
Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs

@@ -61,6 +61,9 @@ internal partial class WindowsOutput : IConsoleOutput
 
     private readonly nint _screenBuffer;
 
+    // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
+    private TextStyle _redrawTextStyle = TextStyle.None;
+
     public WindowsOutput ()
     {
         Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
@@ -233,6 +236,8 @@ internal partial class WindowsOutput : IConsoleOutput
                     prev = attr;
                     EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
                     EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
+                    EscSeqUtils.CSI_AppendTextStyleChange (stringBuilder, _redrawTextStyle, attr.TextStyle);
+                    _redrawTextStyle = attr.TextStyle;
                 }
 
                 if (info.Char != '\x1b')

+ 5 - 0
Terminal.Gui/Drawing/Attribute.cs

@@ -34,6 +34,10 @@ public readonly record struct Attribute : IEqualityOperators<Attribute, Attribut
     [JsonConverter (typeof (ColorJsonConverter))]
     public Color Background { get; }
 
+    // TODO: Add constructors which permit including a TextStyle.
+    /// <summary>The text style (bold, italic, underlined, etc.).</summary>
+    public TextStyle TextStyle { get; init; } = TextStyle.None;
+
     /// <summary>Initializes a new instance with default values.</summary>
     public Attribute ()
     {
@@ -103,6 +107,7 @@ public readonly record struct Attribute : IEqualityOperators<Attribute, Attribut
     /// <inheritdoc/>
     public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); }
 
+    // TODO: Add TextStyle to Attribute.ToString(), modify unit tests to account
     /// <inheritdoc/>
     public override string ToString ()
     {

+ 79 - 0
Terminal.Gui/Drawing/TextStyle.cs

@@ -0,0 +1,79 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Defines non-color text style flags for an <see cref="Attribute"/>.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Only a subset of ANSI SGR (Select Graphic Rendition) styles are represented.
+///         Styles that are poorly supported, non-visual, or redundant with other APIs are omitted.
+///     </para>
+///     <para>
+///         Multiple styles can be combined using bitwise operations. Use <see cref="Attribute.TextStyle"/>
+///         to get or set these styles on an <see cref="Attribute"/>.
+///     </para>
+///     <para>
+///         Note that <see cref="TextStyle.Bold"/> and <see cref="TextStyle.Faint"/> may be mutually exclusive depending on
+///         the user's terminal and its settings. For instance, if a terminal displays faint text as a darker color, and
+///         bold text as a lighter color, then both cannot
+///         be shown at the same time, and it will be up to the terminal to decide which to display.
+///     </para>
+/// </remarks>
+[Flags]
+public enum TextStyle : byte
+{
+    /// <summary>
+    ///     No text style.
+    /// </summary>
+    /// <remarks>Corresponds to no active SGR styles.</remarks>
+    None = 0b_0000_0000,
+
+    /// <summary>
+    ///     Bold text.
+    /// </summary>
+    /// <remarks>
+    ///     SGR code: 1 (Bold). May be mutually exclusive with <see cref="TextStyle.Faint"/>, see <see cref="TextStyle"/>
+    ///     remarks.
+    /// </remarks>
+    Bold = 0b_0000_0001,
+
+    /// <summary>
+    ///     Faint (dim) text.
+    /// </summary>
+    /// <remarks>
+    ///     SGR code: 2 (Faint). Not widely supported on all terminals. May be mutually exclusive with
+    ///     <see cref="TextStyle.Bold"/>, see
+    ///     <see cref="TextStyle"/> remarks.
+    /// </remarks>
+    Faint = 0b_0000_0010,
+
+    /// <summary>
+    ///     Italic text.
+    /// </summary>
+    /// <remarks>SGR code: 3 (Italic). Some terminals may not support italic rendering.</remarks>
+    Italic = 0b_0000_0100,
+
+    /// <summary>
+    ///     Underlined text.
+    /// </summary>
+    /// <remarks>SGR code: 4 (Underline).</remarks>
+    Underline = 0b_0000_1000,
+
+    /// <summary>
+    ///     Slow blinking text.
+    /// </summary>
+    /// <remarks>SGR code: 5 (Slow Blink). Support varies; blinking is often disabled in modern terminals.</remarks>
+    Blink = 0b_0001_0000,
+
+    /// <summary>
+    ///     Reverse video (swaps foreground and background colors).
+    /// </summary>
+    /// <remarks>SGR code: 7 (Reverse Video).</remarks>
+    Reverse = 0b_0010_0000,
+
+    /// <summary>
+    ///     Strikethrough (crossed-out) text.
+    /// </summary>
+    /// <remarks>SGR code: 9 (Crossed-out / Strikethrough).</remarks>
+    Strikethrough = 0b_0100_0000
+}