Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 41 additions & 3 deletions src/ModernOverlay.UI/AdvancedControls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ public sealed class TabControl : UiPanel
{
private const float HeaderHeight = 30f;
private int selectedIndex = -1;
private float[] renderedHeaderWidths = [];
private string[] renderedHeaderTexts = [];

/// <summary>
/// Initializes a new instance of the <see cref="TabControl"/> class.
Expand Down Expand Up @@ -387,19 +389,22 @@ protected override void RenderCore(UiRenderContext context)
context.Draw.Draw.Line(new PointF(Bounds.X, Bounds.Y + HeaderHeight - 1f), new PointF(Bounds.X + Bounds.Width, Bounds.Y + HeaderHeight - 1f), context.Theme.Accent);
}

EnsureRenderedHeaderCache();
float x = Bounds.X;
for (int index = 0; index < Items.Count; index++)
{
TabItem item = Items[index];
float width = item.Header.Length * context.Theme.Theme.FontSize * 0.62f + 24f;
SizeF textSize = context.Draw.Measure.Text(item.Header, context.Theme.Font);
float width = textSize.Width + 24f;
renderedHeaderWidths[index] = width;
renderedHeaderTexts[index] = item.Header;
RectF tab = new(x, Bounds.Y, width, HeaderHeight);
bool itemEnabled = enabled && item.IsEnabled;
if (index == SelectedIndex && itemEnabled)
{
context.Draw.Fill.Rectangle(new RectF(tab.X + 8f, tab.Y + tab.Height - 4f, MathF.Max(0f, tab.Width - 16f), 4f), context.Theme.Accent);
}

SizeF textSize = context.Draw.Measure.Text(item.Header, context.Theme.Font);
float textX = tab.X + MathF.Max(0f, tab.Width - textSize.Width) / 2f;
context.Draw.Draw.Text(item.Header, context.Theme.Font, itemEnabled ? context.Theme.Foreground : context.Theme.Disabled, new PointF(textX, tab.Y + 7f));
x += width + 2f;
Expand Down Expand Up @@ -467,7 +472,7 @@ private int HeaderIndexAt(PointF point)
float x = Bounds.X;
for (int index = 0; index < Items.Count; index++)
{
float width = Items[index].Header.Length * (Root?.ThemeResources.Theme.FontSize ?? UiTheme.Default.FontSize) * 0.62f + 24f;
float width = HeaderWidth(index);
RectF header = new(x, Bounds.Y, width, HeaderHeight);
if (UiGeometry.ContainsInputBand(header, point))
{
Expand All @@ -480,6 +485,39 @@ private int HeaderIndexAt(PointF point)
return -1;
}

private float HeaderWidth(int index)
{
string header = Items[index].Header;
return TryGetRenderedHeaderWidth(index, header, out float width)
? width
: header.Length * (Root?.ThemeResources.Theme.FontSize ?? UiTheme.Default.FontSize) * 0.62f + 24f;
}

private bool TryGetRenderedHeaderWidth(int index, string header, out float width)
{
bool hasWidth = renderedHeaderWidths.Length == Items.Count
&& renderedHeaderTexts.Length == Items.Count
&& index >= 0
&& index < renderedHeaderWidths.Length
&& string.Equals(renderedHeaderTexts[index], header, StringComparison.Ordinal);

width = hasWidth ? renderedHeaderWidths[index] : 0f;
return hasWidth;
}

private void EnsureRenderedHeaderCache()
{
if (renderedHeaderWidths.Length != Items.Count)
{
renderedHeaderWidths = Items.Count == 0 ? [] : new float[Items.Count];
}

if (renderedHeaderTexts.Length != Items.Count)
{
renderedHeaderTexts = Items.Count == 0 ? [] : new string[Items.Count];
}
}

private void MoveSelection(int direction)
{
if (Items.Count == 0)
Expand Down
32 changes: 30 additions & 2 deletions tests/ModernOverlay.Tests/OverlayUiTabSegmentedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task TabPointerBoundaryKeepsPreviousRenderedHeader()
using OverlayUiRoot ui = OverlayUi.Attach(overlay, new OverlayUiOptions { RegisterInputRegions = false });
UiTabControl tabs = CreateTabs(out _, out _, out _);
ui.Root.Children.Add(tabs);
ui.Render(new DrawContext());
ui.Render(new DrawContext(new RecordingDrawCommandSink()));

float boundaryX = 10f + TabHeaderWidth("One");
ClickUi(ui, new PointF(boundaryX, 20f));
Expand Down Expand Up @@ -118,6 +118,32 @@ public async Task TabHeadersRenderTextCenteredByDefault()
Assert.AreEqual(expectedX, oneOrigin.X, 0.001f);
}

[TestMethod]
[TestCategory("WindowsIntegration")]
public async Task TabHeaderHitTestingUsesMeasuredTextWidth()
{
await using OverlayWindow overlay = await CreateOverlayAsync();
using OverlayUiRoot ui = OverlayUi.Attach(overlay, new OverlayUiOptions { RegisterInputRegions = false });
UiTabControl tabs = new()
{
Width = 220f,
Height = 120f,
MinWidth = 0f,
MinHeight = 0f,
};
Canvas.SetLeft(tabs, 10f);
Canvas.SetTop(tabs, 10f);
tabs.Add("WWW", new ProbeElement());
tabs.Add("Two", new ProbeElement());
tabs.SelectedIndex = 1;
ui.Root.Children.Add(tabs);
ui.Render(new DrawContext(new RecordingDrawCommandSink()));

ClickUi(ui, new PointF(130f, 20f));

Assert.AreEqual(0, tabs.SelectedIndex);
}

[TestMethod]
[TestCategory("WindowsIntegration")]
public async Task SegmentedControlPointerAndKeyboardSelection()
Expand Down Expand Up @@ -353,7 +379,9 @@ public SizeF MeasureTextLayout(TextLayoutHandle layout)
=> new(MeasureTextWidth(layout.Text), layout.Font.Options.Size);

public static float MeasureTextWidth(string text)
=> text.Length * UiTheme.Default.FontSize * 0.62f;
=> text == "WWW"
? 120f
: text.Length * UiTheme.Default.FontSize * 0.62f;

private void AddPrimitive()
{
Expand Down
Loading