Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 109 additions & 14 deletions Terminal.Gui/Views/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
/// </remarks>
public class Button : View, IDesignable, IAcceptTarget
{
private readonly TextFormatter _interiorTextFormatter = new ();
private readonly Rune _leftBracket;
private readonly Rune _leftDefault;
private readonly Rune _rightBracket;
Expand Down Expand Up @@ -71,6 +72,10 @@ public Button ()
_leftDefault = Glyphs.LeftDefaultIndicator;
_rightDefault = Glyphs.RightDefaultIndicator;

_interiorTextFormatter.Alignment = Alignment.Center;
_interiorTextFormatter.VerticalAlignment = Alignment.Center;
_interiorTextFormatter.HotKeySpecifier = HotKeySpecifier;

Height = Dim.Auto (DimAutoStyle.Text);
Width = Dim.Auto (DimAutoStyle.Text);

Expand Down Expand Up @@ -167,13 +172,18 @@ private void Button_TitleChanged (object? sender, EventArgs<string> e)
{
base.Text = e.Value;
TextFormatter.HotKeySpecifier = HotKeySpecifier;
_interiorTextFormatter.HotKeySpecifier = HotKeySpecifier;
}

/// <inheritdoc/>
public override string Text { get => Title; set => base.Text = Title = value; }

/// <inheritdoc/>
public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; }
public override Rune HotKeySpecifier
{
get => base.HotKeySpecifier;
set => _interiorTextFormatter.HotKeySpecifier = TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value;
}

/// <inheritdoc/>
public bool IsDefault
Expand Down Expand Up @@ -208,26 +218,64 @@ public bool IsDefault
protected override void UpdateTextFormatterText ()
{
base.UpdateTextFormatterText ();
TextFormatter.Text = GetDecoratedText ();
}

if (NoDecorations)
/// <inheritdoc/>
protected override bool OnDrawingText (DrawContext? context)
{
if (NoDecorations || Driver is null)
{
TextFormatter.Text = Text;
return base.OnDrawingText (context);
}
else if (IsDefault)

Rectangle drawRect = new (ContentToScreen (Point.Empty), GetContentSize ());

if (drawRect.Width < 2 || drawRect.Height < 1)
{
TextFormatter.Text = $"{_leftBracket}{_leftDefault} {Text} {_rightDefault}{_rightBracket}";
return base.OnDrawingText (context);
}
else

Rectangle interiorRect = new (drawRect.X + 1, drawRect.Y, drawRect.Width - 2, drawRect.Height);
Attribute normalAttr = HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal);
Attribute hotAttr = HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal);
string interiorText = GetInteriorText ();

_interiorTextFormatter.Text = interiorText;
_interiorTextFormatter.Alignment = TextAlignment;
_interiorTextFormatter.VerticalAlignment = VerticalTextAlignment;
_interiorTextFormatter.Direction = TextDirection;
_interiorTextFormatter.PreserveTrailingSpaces = PreserveTrailingSpaces;
_interiorTextFormatter.ConstrainToWidth = interiorRect.Width;
_interiorTextFormatter.ConstrainToHeight = interiorRect.Height;
Comment thread
YourRobotOverlord marked this conversation as resolved.
Outdated

Comment thread
YourRobotOverlord marked this conversation as resolved.
Outdated
Region? interiorDrawRegion = (interiorRect.Width > 0 && !string.IsNullOrEmpty (interiorText))
? _interiorTextFormatter.GetDrawRegion (interiorRect)
: null;

context?.AddDrawnRegion (new Region (drawRect));

int delimiterRow = GetDelimiterRow (drawRect, interiorDrawRegion);

// Fill the entire content area with the focus/normal attribute to ensure continuous
// highlight across all rows and across the bracket columns.
Driver.SetAttribute (normalAttr);
Driver.FillRect (drawRect);

Driver.Move (drawRect.X, delimiterRow);
Driver.AddRune (_leftBracket);

Driver.Move (drawRect.X + drawRect.Width - 1, delimiterRow);
Driver.AddRune (_rightBracket);

if (interiorDrawRegion is not null)
{
if (NoPadding)
{
TextFormatter.Text = $"{_leftBracket}{Text}{_rightBracket}";
}
else
{
TextFormatter.Text = $"{_leftBracket} {Text} {_rightBracket}";
}
_interiorTextFormatter.Draw (Driver, interiorRect, normalAttr, hotAttr, Rectangle.Empty);
}

SetSubViewNeedsDrawDownHierarchy ();

return true;
}

/// <inheritdoc/>
Expand All @@ -237,4 +285,51 @@ public bool EnableForDesign ()

return true;
}

// GetDecoratedText (called by UpdateTextFormatterText for Dim.Auto sizing) and GetInteriorText
// (called by OnDrawingText for rendering) must remain in sync when modifying button text formatting.
private string GetDecoratedText ()
{
if (NoDecorations)
{
return Text;
}

return $"{_leftBracket}{GetInteriorText ()}{_rightBracket}";
}

private string GetInteriorText ()
{
if (IsDefault)
{
return $"{_leftDefault} {Text} {_rightDefault}";
}

if (NoPadding)
{
return Text;
}

return $" {Text} ";
}

private int GetDelimiterRow (Rectangle drawRect, Region? interiorDrawRegion)
{
if (interiorDrawRegion is not null)
{
Rectangle interiorBounds = interiorDrawRegion.GetBounds ();

if (!interiorBounds.IsEmpty)
{
return interiorBounds.Y;
}
}

return VerticalTextAlignment switch
{
Alignment.End => drawRect.Y + drawRect.Height - 1,
Alignment.Center => drawRect.Y + (drawRect.Height - 1) / 2,
_ => drawRect.Y
};
}
}
78 changes: 78 additions & 0 deletions Tests/UnitTestsParallelizable/Views/ButtonDrawingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using UnitTests;

namespace ViewsTests;

public class ButtonDrawingTests (ITestOutputHelper output) : TestDriverBase
{
// Copilot
[Theory]
[InlineData (false)]
[InlineData (true)]
public void FixedWidth_Anchors_Delimiters_To_Edges (bool isDefault)
{
const int width = 10;
string expected = isDefault
? $"{Glyphs.LeftBracket} {Glyphs.LeftDefaultIndicator} OK {Glyphs.RightDefaultIndicator} {Glyphs.RightBracket}"
: $"{Glyphs.LeftBracket} OK {Glyphs.RightBracket}";

using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.Driver!.SetScreenSize (width, 1);

Runnable runnable = new () { Width = width, Height = 1 };
app.Begin (runnable);

Button button = new ()
{
Text = "_OK",
X = 0,
Y = 0,
Width = width,
Height = 1,
IsDefault = isDefault,
ShadowStyle = null
};

runnable.Add (button);
app.LayoutAndDraw ();

DriverAssert.AssertDriverContentsWithFrameAre (expected, output, app.Driver);
}

// Copilot
[Fact]
public void Focused_FixedWidth_Button_Highlight_Is_Continuous ()
{
const int width = 10;

using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.Driver!.SetScreenSize (width, 1);
Comment thread
YourRobotOverlord marked this conversation as resolved.

Runnable runnable = new () { Width = width, Height = 1 };
app.Begin (runnable);

Button button = new ()
{
Text = "_OK",
X = 0,
Y = 0,
Width = width,
Height = 1,
ShadowStyle = null
};

runnable.Add (button);
button.SetFocus ();
app.LayoutAndDraw ();

DriverAssert.AssertDriverContentsWithFrameAre ($"{Glyphs.LeftBracket} OK {Glyphs.RightBracket}", output, app.Driver);
DriverAssert.AssertDriverAttributesAre (
"0000100000",
output,
app.Driver,
button.GetAttributeForRole (VisualRole.Focus),
button.GetAttributeForRole (VisualRole.HotFocus)
);
}
}
Loading