diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index adc463cc2..e0b2e2aed 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1849,6 +1849,136 @@ public static class EscSeqUtils #endregion + #region Text Styles + + /// + /// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one to another. + /// + /// to add escape sequence to. + /// Previous to change away from. + /// Next to change to. + /// + /// + /// 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. + /// + /// + 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 (); + + 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 /// diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 69defb82e..fb93f6405 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/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; + /// /// Creates a new instance of the class. /// @@ -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++; diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 248c266fa..1b6295985 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/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; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 81142fb83..4ec616616 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/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') diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs index 28097f377..cba89e682 100644 --- a/Terminal.Gui/Drawing/Attribute.cs +++ b/Terminal.Gui/Drawing/Attribute.cs @@ -34,6 +34,10 @@ public readonly record struct Attribute : IEqualityOperatorsThe text style (bold, italic, underlined, etc.). + public TextStyle TextStyle { get; init; } = TextStyle.None; + /// Initializes a new instance with default values. public Attribute () { @@ -103,6 +107,7 @@ public readonly record struct Attribute : IEqualityOperators public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); } + // TODO: Add TextStyle to Attribute.ToString(), modify unit tests to account /// public override string ToString () { diff --git a/Terminal.Gui/Drawing/TextStyle.cs b/Terminal.Gui/Drawing/TextStyle.cs new file mode 100644 index 000000000..5a7fbb806 --- /dev/null +++ b/Terminal.Gui/Drawing/TextStyle.cs @@ -0,0 +1,79 @@ +namespace Terminal.Gui; + +/// +/// Defines non-color text style flags for an . +/// +/// +/// +/// 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. +/// +/// +/// Multiple styles can be combined using bitwise operations. Use +/// to get or set these styles on an . +/// +/// +/// Note that and 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. +/// +/// +[Flags] +public enum TextStyle : byte +{ + /// + /// No text style. + /// + /// Corresponds to no active SGR styles. + None = 0b_0000_0000, + + /// + /// Bold text. + /// + /// + /// SGR code: 1 (Bold). May be mutually exclusive with , see + /// remarks. + /// + Bold = 0b_0000_0001, + + /// + /// Faint (dim) text. + /// + /// + /// SGR code: 2 (Faint). Not widely supported on all terminals. May be mutually exclusive with + /// , see + /// remarks. + /// + Faint = 0b_0000_0010, + + /// + /// Italic text. + /// + /// SGR code: 3 (Italic). Some terminals may not support italic rendering. + Italic = 0b_0000_0100, + + /// + /// Underlined text. + /// + /// SGR code: 4 (Underline). + Underline = 0b_0000_1000, + + /// + /// Slow blinking text. + /// + /// SGR code: 5 (Slow Blink). Support varies; blinking is often disabled in modern terminals. + Blink = 0b_0001_0000, + + /// + /// Reverse video (swaps foreground and background colors). + /// + /// SGR code: 7 (Reverse Video). + Reverse = 0b_0010_0000, + + /// + /// Strikethrough (crossed-out) text. + /// + /// SGR code: 9 (Crossed-out / Strikethrough). + Strikethrough = 0b_0100_0000 +}