Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ protected override CreateParams CreateParams
cp.Style |= (int)PInvoke.TCS_OWNERDRAWFIXED;
}

// Enable owner-draw for vertical tabs in dark mode since standard themes don't support it
else if (Application.IsDarkModeEnabled &&
(_alignment is TabAlignment.Left or TabAlignment.Right))
{
cp.Style |= (int)PInvoke.TCS_OWNERDRAWFIXED;
}

if (ShowToolTips && !DesignMode)
{
cp.Style |= (int)PInvoke.TCS_TOOLTIPS;
Expand Down Expand Up @@ -1301,7 +1308,14 @@ private void ApplyDarkModeOnDemand()
// We need to avoid to apply the DarkMode theme twice on handle recreate.
if (!_suspendDarkModeChange && Application.IsDarkModeEnabled)
{
PInvoke.SetWindowTheme(HWND, null, $"{DarkModeIdentifier}::{BannerContainerThemeIdentifier}");
// For horizontal tabs, apply the standard dark mode theme
// For vertical tabs, we use owner-draw mode (set in CreateParams) so don't apply theme to main control
if (_alignment is TabAlignment.Top or TabAlignment.Bottom)
{
PInvoke.SetWindowTheme(HWND, null, $"{DarkModeIdentifier}::{BannerContainerThemeIdentifier}");
}

// Apply theme to child windows for both horizontal and vertical tabs
PInvokeCore.EnumChildWindows(this, StyleChildren);
}

Expand Down Expand Up @@ -1334,7 +1348,106 @@ protected override void OnHandleDestroyed(EventArgs e)
/// </summary>
protected virtual void OnDrawItem(DrawItemEventArgs e)
{
_onDrawItem?.Invoke(this, e);
// If we're in automatic owner-draw mode for dark mode vertical tabs,
// provide default rendering if user hasn't attached a handler
if (Application.IsDarkModeEnabled &&
(_alignment is TabAlignment.Left or TabAlignment.Right) &&
_drawMode != TabDrawMode.OwnerDrawFixed &&
_onDrawItem is null)
{
DrawDarkModeTab(e);
}
else
{
_onDrawItem?.Invoke(this, e);
}
}

private void DrawDarkModeTab(DrawItemEventArgs e)
{
Color backColor = (e.State & DrawItemState.Selected) != 0
? SystemColors.ControlDark
: SystemColors.ControlLight;

Color borderColor = SystemColors.ControlDark;
Color textColor = SystemColors.ControlText;

// Draw tab background
using (SolidBrush brush = new(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}

// Draw tab border
using (Pen pen = new(borderColor))
{
e.Graphics.DrawRectangle(pen, e.Bounds.X, e.Bounds.Y, e.Bounds.Width, e.Bounds.Height);
}

// Draw tab text
if (e.Index >= 0 && e.Index < TabPages.Count)
{
TabPage page = TabPages[e.Index];
string text = page.Text;

// Create StringFormat for text alignment (reused for both horizontal and vertical)
using StringFormat format = new()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter
};

// For vertical tabs (Left/Right alignment), rotate the text 90/-90 degrees
if (_alignment is TabAlignment.Left or TabAlignment.Right)
{
// Save the current graphics state
var state = e.Graphics.Save();

try
{
float angle = _alignment == TabAlignment.Left ? -90 : 90;

// Calculate the center point for rotation
float centerX = e.Bounds.X + e.Bounds.Width / 2f;
float centerY = e.Bounds.Y + e.Bounds.Height / 2f;

// Apply rotation transform around center point
e.Graphics.TranslateTransform(centerX, centerY);
e.Graphics.RotateTransform(angle);
e.Graphics.TranslateTransform(-centerX, -centerY);

// For rotated text, swap width and height since the text will be rendered vertically
// The offset calculation centers the rotated text rectangle within the original tab bounds
Rectangle textBounds = new Rectangle(
e.Bounds.X + (e.Bounds.Width - e.Bounds.Height) / 2,
e.Bounds.Y + (e.Bounds.Height - e.Bounds.Width) / 2,
e.Bounds.Height,
e.Bounds.Width);

// Use Graphics.DrawString for proper rotation support (TextRenderer doesn't respect transforms)
using SolidBrush textBrush = new(textColor);
e.Graphics.DrawString(text, Font, textBrush, textBounds, format);
}
finally
{
// Restore the graphics state
e.Graphics.Restore(state);
}
}
else
{
// Horizontal tabs - no rotation needed
using SolidBrush textBrush = new(textColor);
e.Graphics.DrawString(text, Font, textBrush, e.Bounds, format);
}

// Draw focus rectangle if needed
if ((e.State & DrawItemState.Focus) != 0)
{
ControlPaint.DrawFocusRectangle(e.Graphics, e.Bounds);
}
}
}

/// <summary>
Expand Down Expand Up @@ -2061,6 +2174,77 @@ protected override unsafe void WndProc(ref Message m)
{
switch (m.MsgInternal)
{
case PInvokeCore.WM_PAINT:
// After default paint, draw dark border around TabPage content area for vertical tabs in dark mode
base.WndProc(ref m);
if (Application.IsDarkModeEnabled &&
(_alignment is TabAlignment.Left or TabAlignment.Right) &&
_drawMode != TabDrawMode.OwnerDrawFixed)
{
try
{
using Graphics g = Graphics.FromHwnd(HWND);
Rectangle displayRect = DisplayRectangle;
using SolidBrush borderBrush = new(SystemColors.ControlDark);

int borderThickness = 4;
// Top border
g.FillRectangle(borderBrush,
displayRect.X - borderThickness,
displayRect.Y - borderThickness,
displayRect.Width + borderThickness * 2,
borderThickness);
// Bottom border
g.FillRectangle(borderBrush,
displayRect.X - borderThickness,
displayRect.Bottom,
displayRect.Width + borderThickness * 2,
borderThickness);
// Left border
g.FillRectangle(borderBrush,
displayRect.X - borderThickness,
displayRect.Y - borderThickness,
borderThickness,
displayRect.Height + borderThickness * 2);
// Right border
g.FillRectangle(borderBrush,
displayRect.Right,
displayRect.Y - borderThickness,
0,
displayRect.Height + borderThickness * 2);
}
catch
{
// Ignore painting errors
}
}

return;

case PInvokeCore.WM_ERASEBKGND:
// Paint the tab strip background dark for vertical tabs in dark mode
if (Application.IsDarkModeEnabled &&
(_alignment is TabAlignment.Left or TabAlignment.Right) &&
_drawMode != TabDrawMode.OwnerDrawFixed &&
m.WParamInternal != (WPARAM)0)
{
try
{
using Graphics g = Graphics.FromHdc((nint)m.WParamInternal);
// Use darker background color for tab strip (was used for content area)
using SolidBrush brush = new(SystemColors.Control);
g.FillRectangle(brush, ClientRectangle);
m.ResultInternal = (LRESULT)1;
return;
}
catch
{
// If Graphics creation fails, fall through to default handling
}
}

break;

case MessageId.WM_REFLECT_DRAWITEM:
WmReflectDrawItem(ref m);
break;
Expand Down