From 5cc202dea7a5c3887f5a82065625d5360a9e7770 Mon Sep 17 00:00:00 2001 From: Magomed Date: Tue, 14 Jul 2026 20:32:54 +0300 Subject: [PATCH] Polish cart tabs and dialog chrome for light/dark themes. Add ModernTabControl/SplitContainer for cart UI without system borders, fix button corner artifacts and dialog text contrast, and use TitleBarTop green for auth header in light mode. Co-authored-by: Cursor --- .../Classes/UiThemeHelper.cs | 16 +- .../UserControls/UCOrderItems.cs | 2 + .../UserControls/UCPriceList.Designer.cs | 14 +- .../UserControls/UCPriceList.cs | 14 +- src/Elfisa.UI/Controls/ModernButton.cs | 87 +++- src/Elfisa.UI/Controls/ModernDataGridView.cs | 39 +- .../Controls/ModernSplitContainer.cs | 24 ++ src/Elfisa.UI/Controls/ModernTabControl.cs | 389 ++++++++++++++++++ src/Elfisa.UI/Dialogs/ModernMessageForm.cs | 36 +- src/Elfisa.UI/Helpers/GraphicsExtensions.cs | 66 +++ .../Helpers/SkillGradientButtonPaint.cs | 44 +- src/Elfisa.UI/Helpers/WindowChromeHelper.cs | 15 +- src/Elfisa.UI/Theming/ThemeRenderer.cs | 12 +- 13 files changed, 699 insertions(+), 59 deletions(-) create mode 100644 src/Elfisa.UI/Controls/ModernSplitContainer.cs create mode 100644 src/Elfisa.UI/Controls/ModernTabControl.cs diff --git a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs index 22ba007..e73634a 100644 --- a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs +++ b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs @@ -91,6 +91,14 @@ namespace Электронная_Фармация.Classes label.ForeColor = theme.TextPrimary; label.Font = theme.FontRegular13; } + else if (control is ModernTabControl modernTabControl) + { + modernTabControl.ApplyTheme(theme); + } + else if (control is ModernSplitContainer modernSplitContainer) + { + modernSplitContainer.ApplyTheme(theme); + } else if (control is TabControl tabControl) { tabControl.BackColor = theme.BackgroundSecondary; @@ -101,7 +109,13 @@ namespace Электронная_Фармация.Classes toolStrip.BackColor = theme.TitleBarBottom; toolStrip.ForeColor = Color.White; } - else if (control is Panel || control is UserControl || control is TableLayoutPanel || control is SplitContainer) + else if (control is SplitContainer splitContainer) + { + splitContainer.BackColor = theme.Mode == ThemeMode.Dark ? theme.BorderSubtle : theme.Border; + splitContainer.Panel1.BackColor = theme.BackgroundPrimary; + splitContainer.Panel2.BackColor = theme.BackgroundPrimary; + } + else if (control is Panel || control is UserControl || control is TableLayoutPanel) { if (control.Tag as string == "keep-bg") { diff --git a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs index 0894b74..f59a5f9 100644 --- a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs +++ b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs @@ -11,6 +11,7 @@ using System.Data.SQLite; using Электронная_Фармация.UserControls; using System.Data.Common; using Электронная_Фармация.Classes; +using Elfisa.UI.Theming; namespace Электронная_Фармация.UserControls { @@ -223,6 +224,7 @@ and ConsigneesName = '{consigneeName}' UiThemeHelper.ApplyToControlTree(this); _consigneeName = ParentForm?.ConsigneeName ?? string.Empty; loadDataAboutOrderV2(_supplierName); + dgvOrder.ApplyTheme(ThemeManager.Colors); } private void btnEraseOrder_Click(object sender, EventArgs e) diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs index 1d696dc..cb0495e 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs @@ -32,7 +32,7 @@ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(UCPriceList)); this.PContentPriceList = new System.Windows.Forms.Panel(); - this.SplitPriceList = new System.Windows.Forms.SplitContainer(); + this.SplitPriceList = new Elfisa.UI.Controls.ModernSplitContainer(); this.TLPPriceList = new System.Windows.Forms.TableLayoutPanel(); this.dgvPriceList = new Elfisa.UI.Controls.ModernDataGridView(); this.panel1 = new System.Windows.Forms.Panel(); @@ -45,7 +45,7 @@ this.label3 = new System.Windows.Forms.Label(); this.POrderInfo = new System.Windows.Forms.Panel(); this.dgvOrder = new Elfisa.UI.Controls.ModernDataGridView(); - this.tabOrderItems = new System.Windows.Forms.TabControl(); + this.tabOrderItems = new Elfisa.UI.Controls.ModernTabControl(); this.lblSumOfOrder = new System.Windows.Forms.Label(); this.btnEraseOrder = new Elfisa.UI.Controls.ModernButton(); this.btnSendOrder = new Elfisa.UI.Controls.ModernButton(); @@ -100,6 +100,7 @@ this.SplitPriceList.Panel2.Controls.Add(this.POrderInfo); this.SplitPriceList.Size = new System.Drawing.Size(1053, 532); this.SplitPriceList.SplitterDistance = 290; + this.SplitPriceList.SplitterWidth = 2; this.SplitPriceList.TabIndex = 0; // // TLPPriceList @@ -288,12 +289,13 @@ this.tabOrderItems.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.tabOrderItems.Dock = System.Windows.Forms.DockStyle.Fill; this.tabOrderItems.Font = new System.Drawing.Font("Segoe UI", 11.25F); - this.tabOrderItems.Location = new System.Drawing.Point(2, 24); + this.tabOrderItems.Location = new System.Drawing.Point(0, 0); this.tabOrderItems.Margin = new System.Windows.Forms.Padding(2); this.tabOrderItems.Name = "tabOrderItems"; this.tabOrderItems.SelectedIndex = 0; - this.tabOrderItems.Size = new System.Drawing.Size(1048, 215); + this.tabOrderItems.Size = new System.Drawing.Size(1048, 238); this.tabOrderItems.TabIndex = 11; // // lblSumOfOrder @@ -485,7 +487,7 @@ #endregion private System.Windows.Forms.Panel PContentPriceList; - private System.Windows.Forms.SplitContainer SplitPriceList; + private Elfisa.UI.Controls.ModernSplitContainer SplitPriceList; private System.Windows.Forms.Panel POrderInfo; private Elfisa.UI.Controls.ModernTextBox txtSearchGoodNameInPriceList; private System.Windows.Forms.Label label2; @@ -499,7 +501,7 @@ private System.Windows.Forms.NumericUpDown numericPercent; private System.Windows.Forms.TableLayoutPanel TLPPriceList; private Elfisa.UI.Controls.ModernTextBox txtConsignee; - private System.Windows.Forms.TabControl tabOrderItems; + private Elfisa.UI.Controls.ModernTabControl tabOrderItems; public Elfisa.UI.Controls.ModernDataGridView dgvPriceList; private Elfisa.UI.Controls.ModernDataGridView dgvOrder; private System.Windows.Forms.Label lblSumOfOrder; diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 85d9e86..c6d4092 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -326,18 +326,18 @@ namespace Электронная_Фармация.UserControls } TabPage pageSupplier = new TabPage($"{tabName}"); + if (SupplierName == string.Empty) + { + pageSupplier.Tag = "cart"; + } + UCOrderItems UCOrderItemsBySuppliers = new UCOrderItems(SupplierName); UCOrderItemsBySuppliers.ParentForm = this; - - pageSupplier.SuspendLayout(); - pageSupplier.ResumeLayout(); - tabOrderItems.TabPages.Add(pageSupplier); - + UCOrderItemsBySuppliers.Dock = DockStyle.Fill; pageSupplier.Controls.Add(UCOrderItemsBySuppliers); - UCOrderItemsBySuppliers.Dock = DockStyle.Fill; - + tabOrderItems.TabPages.Add(pageSupplier); tabOrderItems.SelectTab(pageSupplier); } diff --git a/src/Elfisa.UI/Controls/ModernButton.cs b/src/Elfisa.UI/Controls/ModernButton.cs index a28415f..520a6fc 100644 --- a/src/Elfisa.UI/Controls/ModernButton.cs +++ b/src/Elfisa.UI/Controls/ModernButton.cs @@ -83,6 +83,7 @@ namespace Elfisa.UI.Controls set { _buttonStyle = value; + ApplyRoundedRegion(); Invalidate(); } } @@ -114,7 +115,56 @@ namespace Elfisa.UI.Controls return theme.BackgroundSecondary; } - return Parent?.BackColor ?? theme.ToolbarBackground; + var current = Parent; + while (current != null) + { + if (current.Tag as string == "keep-bg") + { + current = current.Parent; + continue; + } + + var backColor = current.BackColor; + if (backColor.A > 0 && backColor != Color.Transparent) + { + return backColor; + } + + current = current.Parent; + } + + return theme.Surface; + } + + private int GetCornerRadius() + { + if (_headerNavMode || _topNavMode || _sidebarMode) + { + return 0; + } + + if (SkillGradientButtonPaint.UsesGradient(_buttonStyle, _compactIconMode)) + { + return SkillGradientButtonPaint.GetCornerRadius(_buttonStyle, _compactIconMode); + } + + return 6; + } + + private void ApplyRoundedRegion() + { + var radius = GetCornerRadius(); + if (radius <= 0 || Width <= 0 || Height <= 0) + { + Region = null; + return; + } + + var bounds = new Rectangle(0, 0, Width, Height); + using (var path = GraphicsExtensions.CreateRoundedRectangle(bounds, radius)) + { + Region = new Region(path); + } } public ModernButton() @@ -157,6 +207,7 @@ namespace Elfisa.UI.Controls BackColor = ResolveChromeBackground(); } + ApplyRoundedRegion(); Invalidate(); } @@ -200,6 +251,25 @@ namespace Elfisa.UI.Controls base.OnEnabledChanged(e); } + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + ApplyRoundedRegion(); + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + ApplyRoundedRegion(); + } + + protected override void OnParentChanged(EventArgs e) + { + base.OnParentChanged(e); + ApplyRoundedRegion(); + Invalidate(); + } + protected override void OnGotFocus(EventArgs e) { _focused = true; @@ -235,6 +305,14 @@ namespace Elfisa.UI.Controls var bounds = new Rectangle(0, 0, Width, Height); var usesGradient = SkillGradientButtonPaint.UsesGradient(_buttonStyle, _compactIconMode); + if (!_headerNavMode && !_topNavMode && !_sidebarMode) + { + using (var bgBrush = new SolidBrush(ResolveChromeBackground())) + { + g.FillRectangle(bgBrush, bounds); + } + } + if (_headerNavMode) { var headerBounds = new Rectangle(0, 0, Width - 1, Height - 1); @@ -262,11 +340,6 @@ namespace Elfisa.UI.Controls if (usesGradient && !_topNavMode && !_sidebarMode) { - using (var bgBrush = new SolidBrush(ResolveChromeBackground())) - { - g.FillRectangle(bgBrush, bounds); - } - SkillGradientButtonPaint.Paint( g, bounds, @@ -288,7 +361,7 @@ namespace Elfisa.UI.Controls } var flatBounds = new Rectangle(0, 0, Width - 1, Height - 1); - var radius = _compactIconMode ? 6 : 6; + var radius = GetCornerRadius(); var baseColor = ResolveBaseColor(theme); var fill = baseColor; diff --git a/src/Elfisa.UI/Controls/ModernDataGridView.cs b/src/Elfisa.UI/Controls/ModernDataGridView.cs index 2f45d86..b7c4361 100644 --- a/src/Elfisa.UI/Controls/ModernDataGridView.cs +++ b/src/Elfisa.UI/Controls/ModernDataGridView.cs @@ -23,17 +23,34 @@ namespace Elfisa.UI.Controls AllowUserToResizeColumns = true; AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None; ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; - ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single; + ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.None; DoubleBuffered = true; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); CellFormatting += OnGridCellFormatting; ApplyTheme(ThemeManager.Colors); } + private void ApplyBorderStyles(ThemeColors theme) + { + GridColor = theme.GridBorder; + + AdvancedCellBorderStyle.All = DataGridViewAdvancedCellBorderStyle.None; + AdvancedCellBorderStyle.Bottom = DataGridViewAdvancedCellBorderStyle.Single; + + AdvancedColumnHeadersBorderStyle.All = DataGridViewAdvancedCellBorderStyle.None; + AdvancedColumnHeadersBorderStyle.Bottom = DataGridViewAdvancedCellBorderStyle.Single; + + if (Rows.Count > 0) + { + AdvancedRowHeadersBorderStyle.All = DataGridViewAdvancedCellBorderStyle.None; + } + } + public void ApplyTheme(ThemeColors theme) { _theme = theme; BackgroundColor = theme.GridRowBackground; - GridColor = theme.GridBorder; + ApplyBorderStyles(theme); ColumnHeadersHeight = theme.GridHeaderHeight; RowTemplate.Height = theme.GridRowHeight; Font = theme.FontRegular13; @@ -69,6 +86,24 @@ namespace Elfisa.UI.Controls Invalidate(true); } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + var theme = _theme ?? ThemeManager.Colors; + using (var pen = new Pen(theme.GridBorder)) + { + var rect = new Rectangle(0, 0, ClientSize.Width - 1, ClientSize.Height - 1); + e.Graphics.DrawRectangle(pen, rect); + } + } + + protected override void OnRowsAdded(DataGridViewRowsAddedEventArgs e) + { + base.OnRowsAdded(e); + ApplyBorderStyles(_theme ?? ThemeManager.Colors); + } + private void OnGridCellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) diff --git a/src/Elfisa.UI/Controls/ModernSplitContainer.cs b/src/Elfisa.UI/Controls/ModernSplitContainer.cs new file mode 100644 index 0000000..a6e3dc2 --- /dev/null +++ b/src/Elfisa.UI/Controls/ModernSplitContainer.cs @@ -0,0 +1,24 @@ +using System.Drawing; +using System.Windows.Forms; +using Elfisa.UI.Theming; + +namespace Elfisa.UI.Controls +{ + public class ModernSplitContainer : SplitContainer + { + public ModernSplitContainer() + { + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + BorderStyle = BorderStyle.None; + SplitterWidth = 2; + } + + public void ApplyTheme(ThemeColors theme) + { + BackColor = theme.Mode == ThemeMode.Dark ? theme.Border : theme.Border; + Panel1.BackColor = theme.BackgroundPrimary; + Panel2.BackColor = theme.BackgroundPrimary; + Invalidate(true); + } + } +} diff --git a/src/Elfisa.UI/Controls/ModernTabControl.cs b/src/Elfisa.UI/Controls/ModernTabControl.cs new file mode 100644 index 0000000..f02a1e9 --- /dev/null +++ b/src/Elfisa.UI/Controls/ModernTabControl.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; +using Elfisa.UI.Helpers; +using Elfisa.UI.Theming; + +namespace Elfisa.UI.Controls +{ + /// + /// Вкладки в стиле документных. Контент показывается в Panel — без системной рамки TabControl. + /// + public class ModernTabControl : UserControl + { + private const int StripHeight = 40; + private const int TabTop = 6; + private const int TabHeight = 28; + private const int MinTabWidth = 120; + private const int TabGap = 8; + private const int ContentOverlap = 2; + private const int FramePadding = 1; + + private readonly TabControl _inner; + private readonly Panel _contentFrame; + private readonly Panel _contentHost; + private readonly Panel _strip; + private readonly List _tabHitAreas = new List(); + private ThemeColors _theme; + private int _hoverIndex = -1; + private TabPage _displayedPage; + private bool _isReparenting; + + public TabControl.TabPageCollection TabPages => _inner.TabPages; + + public int SelectedIndex + { + get => _inner.SelectedIndex; + set => _inner.SelectedIndex = value; + } + + public int TabCount => _inner.TabCount; + + public ModernTabControl() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw, true); + + _inner = new TabControl + { + Visible = false, + Size = new Size(0, 0), + TabStop = false, + Tag = "keep-bg" + }; + _inner.SelectedIndexChanged += (_, __) => + { + DisplaySelectedTab(); + _strip.Invalidate(); + }; + _inner.ControlAdded += OnInnerControlAdded; + _inner.ControlRemoved += OnInnerControlRemoved; + + _contentHost = new Panel + { + Dock = DockStyle.Fill, + Tag = "keep-bg" + }; + + _contentFrame = new Panel + { + Dock = DockStyle.Fill, + Padding = new Padding(FramePadding), + Tag = "keep-bg" + }; + _contentFrame.Controls.Add(_contentHost); + + _strip = new Panel + { + Dock = DockStyle.Top, + Height = StripHeight, + Tag = "keep-bg" + }; + _strip.Paint += OnStripPaint; + _strip.MouseMove += OnStripMouseMove; + _strip.MouseLeave += OnStripMouseLeave; + _strip.MouseClick += OnStripMouseClick; + + Controls.Add(_inner); + Controls.Add(_contentFrame); + Controls.Add(_strip); + } + + public void SelectTab(TabPage page) + { + var index = _inner.TabPages.IndexOf(page); + if (index < 0) + { + return; + } + + if (_inner.SelectedIndex == index) + { + DisplaySelectedTab(); + } + else + { + _inner.SelectedIndex = index; + } + } + + public void ApplyTheme(ThemeColors theme) + { + _theme = theme; + BackColor = theme.BackgroundSecondary; + _strip.BackColor = theme.BackgroundSecondary; + _contentFrame.BackColor = theme.Border; + _contentHost.BackColor = theme.BackgroundPrimary; + SyncTabPageTheme(); + _strip.Invalidate(); + } + + private void OnInnerControlAdded(object sender, ControlEventArgs e) + { + SyncTabPageTheme(); + _strip.Invalidate(); + } + + private void OnInnerControlRemoved(object sender, ControlEventArgs e) + { + if (e.Control is TabPage page && page == _displayedPage) + { + _displayedPage = null; + } + + if (_inner.TabCount == 0) + { + _displayedPage = null; + _contentHost.Controls.Clear(); + } + + _strip.Invalidate(); + } + + private void DisplaySelectedTab() + { + if (_isReparenting) + { + return; + } + + _isReparenting = true; + try + { + ReturnControlsToTabPage(_displayedPage); + + if (_inner.SelectedIndex < 0 || _inner.SelectedIndex >= _inner.TabCount) + { + _displayedPage = null; + _contentHost.Controls.Clear(); + return; + } + + _displayedPage = _inner.TabPages[_inner.SelectedIndex]; + MoveControlsToHost(_displayedPage); + } + finally + { + _isReparenting = false; + } + } + + private void ReturnControlsToTabPage(TabPage page) + { + if (page == null) + { + return; + } + + var controls = new Control[_contentHost.Controls.Count]; + _contentHost.Controls.CopyTo(controls, 0); + _contentHost.Controls.Clear(); + + foreach (var control in controls) + { + control.Dock = DockStyle.Fill; + page.Controls.Add(control); + } + } + + private void MoveControlsToHost(TabPage page) + { + _contentHost.Controls.Clear(); + + var controls = new Control[page.Controls.Count]; + page.Controls.CopyTo(controls, 0); + page.Controls.Clear(); + + foreach (var control in controls) + { + control.Dock = DockStyle.Fill; + _contentHost.Controls.Add(control); + } + } + + private void OnStripPaint(object sender, PaintEventArgs e) + { + var theme = _theme ?? ThemeManager.Colors; + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + + using (var bgBrush = new SolidBrush(theme.BackgroundSecondary)) + { + g.FillRectangle(bgBrush, _strip.ClientRectangle); + } + + _tabHitAreas.Clear(); + if (_inner.TabCount == 0) + { + DrawStripSeparator(g, theme, null, _strip.Width); + return; + } + + var x = 10; + Rectangle? activeRect = null; + using (var textFont = theme.FontSemibold14 ?? new Font("Segoe UI", 10f, FontStyle.Bold)) + { + for (var i = 0; i < _inner.TabCount; i++) + { + var page = _inner.TabPages[i]; + var textSize = TextRenderer.MeasureText(page.Text, textFont); + var tabWidth = Math.Max(MinTabWidth, textSize.Width + 28); + var tabRect = new Rectangle(x, TabTop, tabWidth, TabHeight); + _tabHitAreas.Add(tabRect); + + var isActive = _inner.SelectedIndex == i; + var isHover = _hoverIndex == i; + + if (isActive) + { + activeRect = new Rectangle(x, TabTop, tabWidth, StripHeight - TabTop + ContentOverlap); + } + else + { + var backColor = theme.Surface; + if (isHover) + { + backColor = GraphicsExtensions.Blend(backColor, theme.AccentPrimarySoft, 0.35f); + } + + using (var brush = new SolidBrush(backColor)) + { + GraphicsExtensions.FillRoundedRectangle(g, brush, tabRect, theme.CornerRadiusSmall); + } + + using (var pen = new Pen(theme.BorderSubtle)) + { + GraphicsExtensions.DrawRoundedRectangle(g, pen, tabRect, theme.CornerRadiusSmall); + } + + TextRenderer.DrawText( + g, + page.Text, + textFont, + tabRect, + theme.TextSecondary, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis); + } + + x += tabWidth + TabGap; + } + } + + if (activeRect.HasValue) + { + var rect = activeRect.Value; + using (var brush = new SolidBrush(theme.BackgroundPrimary)) + { + GraphicsExtensions.FillTopRoundedRectangle(g, brush, rect, theme.CornerRadiusSmall); + } + + var outlineColor = theme.Mode == ThemeMode.Dark ? theme.BorderSubtle : theme.AccentPrimaryBorder; + using (var pen = new Pen(outlineColor, 1f)) + { + GraphicsExtensions.DrawAttachedTabOutline(g, pen, rect, theme.CornerRadiusSmall); + } + + using (var seam = new SolidBrush(theme.BackgroundPrimary)) + { + g.FillRectangle(seam, rect.Left, StripHeight - ContentOverlap - 1, rect.Width, ContentOverlap + 2); + } + + using (var textFont = theme.FontSemibold14 ?? new Font("Segoe UI", 10f, FontStyle.Bold)) + { + var textRect = new Rectangle(rect.X, TabTop, rect.Width, TabHeight); + TextRenderer.DrawText( + g, + _inner.TabPages[_inner.SelectedIndex].Text, + textFont, + textRect, + theme.TextPrimary, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis); + } + } + + DrawStripSeparator(g, theme, activeRect, _strip.Width); + } + + private static void DrawStripSeparator(Graphics g, ThemeColors theme, Rectangle? activeRect, int width) + { + var y = StripHeight - 1; + using (var separator = new Pen(theme.Border)) + { + if (activeRect.HasValue) + { + var rect = activeRect.Value; + if (rect.Left > 0) + { + g.DrawLine(separator, 0, y, rect.Left, y); + } + + if (rect.Right < width) + { + g.DrawLine(separator, rect.Right, y, width, y); + } + } + else + { + g.DrawLine(separator, 0, y, width, y); + } + } + } + + private void OnStripMouseMove(object sender, MouseEventArgs e) + { + var hoverIndex = HitTestTab(e.Location); + if (hoverIndex != _hoverIndex) + { + _hoverIndex = hoverIndex; + _strip.Invalidate(); + } + + _strip.Cursor = hoverIndex >= 0 ? Cursors.Hand : Cursors.Default; + } + + private void OnStripMouseLeave(object sender, EventArgs e) + { + _hoverIndex = -1; + _strip.Cursor = Cursors.Default; + _strip.Invalidate(); + } + + private void OnStripMouseClick(object sender, MouseEventArgs e) + { + var index = HitTestTab(e.Location); + if (index >= 0) + { + SelectedIndex = index; + } + } + + private int HitTestTab(Point location) + { + for (var i = 0; i < _tabHitAreas.Count; i++) + { + if (_tabHitAreas[i].Contains(location)) + { + return i; + } + } + + return -1; + } + + private void SyncTabPageTheme() + { + var theme = _theme ?? ThemeManager.Colors; + foreach (TabPage page in _inner.TabPages) + { + page.BackColor = theme.BackgroundPrimary; + page.ForeColor = theme.TextPrimary; + } + + _strip.Invalidate(); + } + } +} diff --git a/src/Elfisa.UI/Dialogs/ModernMessageForm.cs b/src/Elfisa.UI/Dialogs/ModernMessageForm.cs index b72f28e..1f6bb6c 100644 --- a/src/Elfisa.UI/Dialogs/ModernMessageForm.cs +++ b/src/Elfisa.UI/Dialogs/ModernMessageForm.cs @@ -19,21 +19,21 @@ namespace Elfisa.UI.Dialogs ShowInTaskbar = false; Size = new Size(440, 220); Padding = new Padding(20); - BackColor = ThemeManager.Colors.Surface; var titleLabel = new Label { Text = title, Dock = DockStyle.Top, Height = 34, - Font = ThemeManager.Colors.FontSemibold16 + Font = ThemeManager.Colors.FontSemibold16, + BackColor = Color.Transparent }; var messageLabel = new Label { Text = message, Dock = DockStyle.Fill, Font = ThemeManager.Colors.FontRegular14, - Tag = "muted" + BackColor = Color.Transparent }; var buttonPanel = new FlowLayoutPanel @@ -63,11 +63,41 @@ namespace Elfisa.UI.Dialogs Controls.Add(messageLabel); Controls.Add(buttonPanel); Controls.Add(titleLabel); + + ApplyTheme(ThemeManager.Colors); + } + + private void ApplyTheme(ThemeColors theme) + { + BackColor = theme.Surface; + ForeColor = theme.TextPrimary; + + foreach (Control control in Controls) + { + if (control is Label label) + { + label.ForeColor = theme.TextPrimary; + label.BackColor = Color.Transparent; + } + else if (control is FlowLayoutPanel panel) + { + panel.BackColor = theme.Surface; + panel.ForeColor = theme.TextPrimary; + foreach (Control child in panel.Controls) + { + if (child is ModernButton button) + { + button.ApplyTheme(theme); + } + } + } + } } private void AddButton(FlowLayoutPanel panel, string text, ModernButtonStyle style, DialogResult result) { var button = new ModernButton { Text = text, Width = 100, ButtonStyle = style }; + button.ApplyTheme(ThemeManager.Colors); button.Click += (_, __) => { DialogResult = result; diff --git a/src/Elfisa.UI/Helpers/GraphicsExtensions.cs b/src/Elfisa.UI/Helpers/GraphicsExtensions.cs index a23141a..9bb5573 100644 --- a/src/Elfisa.UI/Helpers/GraphicsExtensions.cs +++ b/src/Elfisa.UI/Helpers/GraphicsExtensions.cs @@ -29,6 +29,72 @@ namespace Elfisa.UI.Helpers return path; } + public static GraphicsPath CreateTopRoundedRectangle(Rectangle bounds, int radius) + { + var path = new GraphicsPath(); + if (radius <= 0 || bounds.Width <= 0 || bounds.Height <= 0) + { + path.AddRectangle(bounds); + return path; + } + + var diameter = radius * 2; + var arc = new Rectangle(bounds.X, bounds.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = bounds.Right - diameter; + path.AddArc(arc, 270, 90); + path.AddLine(bounds.Right, bounds.Y + radius, bounds.Right, bounds.Bottom); + path.AddLine(bounds.Right, bounds.Bottom, bounds.Left, bounds.Bottom); + path.AddLine(bounds.Left, bounds.Bottom, bounds.Left, bounds.Y + radius); + path.CloseFigure(); + return path; + } + + public static void FillTopRoundedRectangle(Graphics graphics, Brush brush, Rectangle bounds, int radius) + { + using (var path = CreateTopRoundedRectangle(bounds, radius)) + { + graphics.FillPath(brush, path); + } + } + + public static void DrawTopRoundedRectangle(Graphics graphics, Pen pen, Rectangle bounds, int radius) + { + using (var path = CreateTopRoundedRectangle(bounds, radius)) + { + graphics.DrawPath(pen, path); + } + } + + public static void DrawAttachedTabOutline(Graphics graphics, Pen pen, Rectangle bounds, int radius) + { + if (bounds.Width <= 0 || bounds.Height <= 0) + { + return; + } + + if (radius <= 0) + { + graphics.DrawLine(pen, bounds.Left, bounds.Top, bounds.Right, bounds.Top); + graphics.DrawLine(pen, bounds.Left, bounds.Top, bounds.Left, bounds.Bottom); + graphics.DrawLine(pen, bounds.Right - 1, bounds.Top, bounds.Right - 1, bounds.Bottom); + return; + } + + using (var path = new GraphicsPath()) + { + var diameter = radius * 2; + var arc = new Rectangle(bounds.X, bounds.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = bounds.Right - diameter; + path.AddArc(arc, 270, 90); + path.AddLine(bounds.Right, bounds.Y + radius, bounds.Right, bounds.Bottom); + graphics.DrawPath(pen, path); + } + + graphics.DrawLine(pen, bounds.Left, bounds.Y + radius, bounds.Left, bounds.Bottom); + } + public static void FillRoundedRectangle(Graphics graphics, Brush brush, Rectangle bounds, int radius) { using (var path = CreateRoundedRectangle(bounds, radius)) diff --git a/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs b/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs index b5ca044..38c9ee6 100644 --- a/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs +++ b/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs @@ -80,41 +80,31 @@ namespace Elfisa.UI.Helpers clientRect.Width - 1, clientRect.Height - 1); - g.SetClip(clientRect); - - if (!enabled) + using (var clipPath = GraphicsExtensions.CreateRoundedRectangle(buttonRect, radius)) { - using (var brush = new SolidBrush(Color.FromArgb(220, 230, 235))) + g.SetClip(clipPath); + + if (!enabled) { - GraphicsExtensions.FillRoundedRectangle(g, brush, buttonRect, radius); + using (var brush = new SolidBrush(Color.FromArgb(220, 230, 235))) + { + GraphicsExtensions.FillRoundedRectangle(g, brush, buttonRect, radius); + } + + g.ResetClip(); + return; } - return; - } + GetGradient(style, hover, pressed, out var top, out var bottom); - // Лёгкая тень — один слой, без чёрной полосы снизу - var shadowRect = new Rectangle(buttonRect.X, buttonRect.Y + 1, buttonRect.Width, buttonRect.Height); - using (var shadowPath = GraphicsExtensions.CreateRoundedRectangle(shadowRect, radius)) - using (var shadowBrush = new SolidBrush(Color.FromArgb(28, 0, 0, 0))) - { - g.FillPath(shadowBrush, shadowPath); - } - - var faceRect = new Rectangle(buttonRect.X, buttonRect.Y, buttonRect.Width, buttonRect.Height - 1); - GetGradient(style, hover, pressed, out var top, out var bottom); - - using (var path = GraphicsExtensions.CreateRoundedRectangle(faceRect, radius)) - using (var brush = new LinearGradientBrush(faceRect, top, bottom, LinearGradientMode.Vertical)) - { - g.FillPath(brush, path); - - using (var highlightPen = new Pen(Color.FromArgb(36, Color.White), 1f)) + using (var path = GraphicsExtensions.CreateRoundedRectangle(buttonRect, radius)) + using (var brush = new LinearGradientBrush(buttonRect, top, bottom, LinearGradientMode.Vertical)) { - g.DrawArc(highlightPen, faceRect.X, faceRect.Y, radius * 2, radius * 2, 180, 90); + g.FillPath(brush, path); } - } - g.ResetClip(); + g.ResetClip(); + } } public static Rectangle GetContentRect(Rectangle clientRect, ModernButtonStyle style, bool compactIconMode, bool enabled) diff --git a/src/Elfisa.UI/Helpers/WindowChromeHelper.cs b/src/Elfisa.UI/Helpers/WindowChromeHelper.cs index 6ca4fb9..c2a9d5f 100644 --- a/src/Elfisa.UI/Helpers/WindowChromeHelper.cs +++ b/src/Elfisa.UI/Helpers/WindowChromeHelper.cs @@ -46,8 +46,8 @@ namespace Elfisa.UI.Helpers TrySetInt(form.Handle, DwmwaUseImmersiveDarkMode, useDarkMode); TrySetInt(form.Handle, DwmwaUseImmersiveDarkModeLegacy, useDarkMode); - var caption = isDark ? theme.TitleBarTop : Color.White; - var text = isDark ? Color.White : Color.Black; + var caption = theme.TitleBarTop; + var text = IsLightCaption(caption) ? Color.Black : Color.White; TrySetUInt(form.Handle, DwmwaCaptionColor, ColorToDwm(caption)); TrySetUInt(form.Handle, DwmwaTextColor, ColorToDwm(text)); @@ -62,18 +62,23 @@ namespace Elfisa.UI.Helpers return; } - var isDark = theme.Mode == ThemeMode.Dark; if (headerPanel != null) { - headerPanel.BackColor = isDark ? theme.TitleBarTop : Color.White; + headerPanel.BackColor = theme.TitleBarTop; } if (headerLabel != null) { - headerLabel.ForeColor = isDark ? Color.White : Color.Black; + headerLabel.ForeColor = IsLightCaption(theme.TitleBarTop) ? Color.Black : Color.White; } } + private static bool IsLightCaption(Color color) + { + var luminance = (0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B); + return luminance > 180; + } + public static void ApplyDialogChrome(Form form, Panel headerPanel = null, Label headerLabel = null) { if (form == null) diff --git a/src/Elfisa.UI/Theming/ThemeRenderer.cs b/src/Elfisa.UI/Theming/ThemeRenderer.cs index 9d4f107..49e0181 100644 --- a/src/Elfisa.UI/Theming/ThemeRenderer.cs +++ b/src/Elfisa.UI/Theming/ThemeRenderer.cs @@ -97,6 +97,14 @@ namespace Elfisa.UI.Theming { sectionHeader.ApplyTheme(colors, sectionHeader.Tag as string == "cart"); } + else if (control is ModernTabControl modernTabControl) + { + modernTabControl.ApplyTheme(colors); + } + else if (control is ModernSplitContainer modernSplitContainer) + { + modernSplitContainer.ApplyTheme(colors); + } else if (control is ModernUploadZone uploadZone) { uploadZone.ApplyTheme(colors); @@ -140,7 +148,9 @@ namespace Elfisa.UI.Theming } else if (control is SplitContainer splitContainer) { - splitContainer.BackColor = colors.BackgroundSecondary; + splitContainer.BackColor = colors.Mode == ThemeMode.Dark ? colors.BorderSubtle : colors.Border; + splitContainer.Panel1.BackColor = colors.BackgroundPrimary; + splitContainer.Panel2.BackColor = colors.BackgroundPrimary; } else if (control.Tag as string == "toolbar") {