// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Specialized; using System.Drawing; using System.Drawing.Text; using System.Windows.Forms.Layout; namespace System.Windows.Forms.ButtonInternal; internal abstract partial class ButtonBaseAdapter { internal partial class LayoutOptions { private static readonly int s_combineCheck = BitVector32.CreateMask(); private static readonly int s_combineImageText = BitVector32.CreateMask(s_combineCheck); private bool _disableWordWrapping; // If this is changed to a property callers will need to be updated // as they modify fields in the Rectangle. public Rectangle Client; public bool GrowBorderBy1PxWhenDefault { get; set; } public bool IsDefault { get; set; } public int BorderSize { get; set; } public int PaddingSize { get; set; } public bool MaxFocus { get; set; } public bool FocusOddEvenFixup { get; set; } public Font Font { get; set; } = null!; public string? Text { get; set; } public Size ImageSize { get; set; } public int CheckSize { get; set; } public int CheckPaddingSize { get; set; } public ContentAlignment CheckAlign { get; set; } public ContentAlignment ImageAlign { get; set; } public ContentAlignment TextAlign { get; set; } public TextImageRelation TextImageRelation { get; set; } public bool HintTextUp { get; set; } public bool TextOffset { get; set; } public bool ShadowedText { get; set; } public bool LayoutRTL { get; set; } public bool VerticalText { get; set; } public bool UseCompatibleTextRendering { get; set; } /// <summary> /// .NET Framework 1.0/1.1 compatibility /// </summary> public bool DotNetOneButtonCompat { get; set; } = true; public TextFormatFlags GdiTextFormatFlags { get; set; } = TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl; public StringFormatFlags GdiPlusFormatFlags { get; set; } public StringTrimming GdiPlusTrimming { get; set; } public HotkeyPrefix GdiPlusHotkeyPrefix { get; set; } /// <summary> /// Horizontal alignment. /// </summary> public StringAlignment GdiPlusAlignment { get; set; } /// <summary> /// Vertical alignment. /// </summary> public StringAlignment GdiPlusLineAlignment { get; set; } /// <summary> /// We don't cache the <see cref="StringFormat"/> itself because we don't have a deterministic way of /// disposing it, instead we cache the flags that make it up and create it on demand so it can be disposed /// by calling code. /// </summary> public StringFormat StringFormat { get { StringFormat format = new() { FormatFlags = GdiPlusFormatFlags, Trimming = GdiPlusTrimming, HotkeyPrefix = GdiPlusHotkeyPrefix, Alignment = GdiPlusAlignment, LineAlignment = GdiPlusLineAlignment }; if (_disableWordWrapping) { format.FormatFlags |= StringFormatFlags.NoWrap; } return format; } set { GdiPlusFormatFlags = value.FormatFlags; GdiPlusTrimming = value.Trimming; GdiPlusHotkeyPrefix = value.HotkeyPrefix; GdiPlusAlignment = value.Alignment; GdiPlusLineAlignment = value.LineAlignment; } } public TextFormatFlags TextFormatFlags => _disableWordWrapping ? GdiTextFormatFlags & ~TextFormatFlags.WordBreak : GdiTextFormatFlags; /// <summary> /// TextImageInset compensates for two factors: 3d text when the button is disabled, /// and moving text on 3d-look buttons. These factors make the text require a couple /// more pixels of space. We inset image by the same amount so they line up. /// </summary> public int TextImageInset { get; set; } = 2; public Padding Padding { get; set; } /// <summary> /// Uses <see cref="CheckAlign"/>, <see cref="ImageAlign"/>, and <see cref="TextAlign"/> to compose /// <paramref name="checkSize"/>, <paramref name="imageSize"/>, and <paramref name="textSize"/> into /// the preferred size. /// </summary> private Size Compose(Size checkSize, Size imageSize, Size textSize) { Composition hComposition = GetHorizontalComposition(); Composition vComposition = GetVerticalComposition(); return new Size( xCompose(hComposition, checkSize.Width, imageSize.Width, textSize.Width), xCompose(vComposition, checkSize.Height, imageSize.Height, textSize.Height)); static int xCompose(Composition composition, int checkSize, int imageSize, int textSize) { switch (composition) { case Composition.NoneCombined: return checkSize + imageSize + textSize; case Composition.CheckCombined: return Math.Max(checkSize, imageSize + textSize); case Composition.TextImageCombined: return Math.Max(imageSize, textSize) + checkSize; case Composition.AllCombined: return Math.Max(Math.Max(checkSize, imageSize), textSize); default: Debug.Fail(string.Format(SR.InvalidArgument, nameof(composition), composition.ToString())); return -7107; } } } /// <summary> /// Uses <see cref="CheckAlign"/>, <see cref="ImageAlign"/>, and <see cref="TextAlign"/> to decompose /// <paramref name="proposedSize"/> into the space left over for text. /// </summary> private Size Decompose(Size checkSize, Size imageSize, Size proposedSize) { Composition hComposition = GetHorizontalComposition(); Composition vComposition = GetVerticalComposition(); return new Size( xDecompose(hComposition, checkSize.Width, imageSize.Width, proposedSize.Width), xDecompose(vComposition, checkSize.Height, imageSize.Height, proposedSize.Height)); static int xDecompose(Composition composition, int checkSize, int imageSize, int proposedSize) { switch (composition) { case Composition.NoneCombined: return proposedSize - (checkSize + imageSize); case Composition.CheckCombined: return proposedSize - imageSize; case Composition.TextImageCombined: return proposedSize - checkSize; case Composition.AllCombined: return proposedSize; default: Debug.Fail(string.Format(SR.InvalidArgument, nameof(composition), composition.ToString())); return -7109; } } } private Composition GetHorizontalComposition() { BitVector32 action = default; // Checks reserve space horizontally if possible, so only AnyLeft/AnyRight prevents combination. action[s_combineCheck] = CheckAlign == ContentAlignment.MiddleCenter || !LayoutUtils.IsHorizontalAlignment(CheckAlign); action[s_combineImageText] = !LayoutUtils.IsHorizontalRelation(TextImageRelation); return (Composition)action.Data; } internal Size GetPreferredSizeCore(Size proposedSize) { // Get space required for border and padding. int linearBorderAndPadding = BorderSize * 2 + PaddingSize * 2; if (GrowBorderBy1PxWhenDefault) { linearBorderAndPadding += 2; } Size bordersAndPadding = new(linearBorderAndPadding, linearBorderAndPadding); proposedSize -= bordersAndPadding; // Get space required for check. int checkSizeLinear = FullCheckSize; Size checkSize = checkSizeLinear > 0 ? new(checkSizeLinear + 1, checkSizeLinear) : Size.Empty; // Get space required for Image - textImageInset compensated for by expanding image. Size textImageInsetSize = new(TextImageInset * 2, TextImageInset * 2); Size requiredImageSize = (ImageSize != Size.Empty) ? ImageSize + textImageInsetSize : Size.Empty; // Pack Text into remaining space proposedSize -= textImageInsetSize; proposedSize = Decompose(checkSize, requiredImageSize, proposedSize); Size textSize = Size.Empty; if (!string.IsNullOrEmpty(Text)) { // When Button.AutoSizeMode is set to GrowOnly TableLayoutPanel expects buttons not to // automatically wrap on word break. If there's enough room for the text to word-wrap then it // will happen but the layout would not be adjusted to allow text wrapping. If someone has a // carriage return in the text we'll honor that for preferred size, but we won't wrap based // on constraints. try { _disableWordWrapping = true; textSize = GetTextSize(proposedSize) + textImageInsetSize; } finally { _disableWordWrapping = false; } } // Combine pieces to get final preferred size. Size requiredSize = Compose(checkSize, ImageSize, textSize); requiredSize += bordersAndPadding; return requiredSize; } private Composition GetVerticalComposition() { BitVector32 action = default; // Checks reserve space horizontally if possible, so only Top/Bottom prevents combination. action[s_combineCheck] = CheckAlign == ContentAlignment.MiddleCenter || !LayoutUtils.IsVerticalAlignment(CheckAlign); action[s_combineImageText] = !LayoutUtils.IsVerticalRelation(TextImageRelation); return (Composition)action.Data; } private int FullBorderSize => OnePixExtraBorder ? BorderSize++ : BorderSize; private bool OnePixExtraBorder => GrowBorderBy1PxWhenDefault && IsDefault; internal LayoutData Layout() { LayoutData layout = new(this) { Client = Client }; // Subtract border size from layout area. int fullBorderSize = FullBorderSize; layout.Face = Rectangle.Inflate(layout.Client, -fullBorderSize, -fullBorderSize); // CheckBounds, CheckArea, Field. CalcCheckmarkRectangle(layout); // ImageBounds, ImageLocation, TextBounds. LayoutTextAndImage(layout); // Focus. if (MaxFocus) { layout.Focus = layout.Field; layout.Focus.Inflate(-1, -1); // Adjust for padding. layout.Focus = LayoutUtils.InflateRect(layout.Focus, Padding); } else { Rectangle textAdjusted = new( layout.TextBounds.X - 1, layout.TextBounds.Y - 1, layout.TextBounds.Width + 2, layout.TextBounds.Height + 3); layout.Focus = ImageSize != Size.Empty ? Rectangle.Union(textAdjusted, layout.ImageBounds) : textAdjusted; } if (FocusOddEvenFixup) { if (layout.Focus.Height % 2 == 0) { layout.Focus.Y++; layout.Focus.Height--; } if (layout.Focus.Width % 2 == 0) { layout.Focus.X++; layout.Focus.Width--; } } return layout; } private TextImageRelation RtlTranslateRelation(TextImageRelation relation) { // If RTL, we swap ImageBeforeText and TextBeforeImage. if (LayoutRTL) { switch (relation) { case TextImageRelation.ImageBeforeText: return TextImageRelation.TextBeforeImage; case TextImageRelation.TextBeforeImage: return TextImageRelation.ImageBeforeText; } } return relation; } internal ContentAlignment RtlTranslateContent(ContentAlignment align) { if (LayoutRTL) { ContentAlignment[][] mapping = [ [ContentAlignment.TopLeft, ContentAlignment.TopRight], [ContentAlignment.MiddleLeft, ContentAlignment.MiddleRight], [ContentAlignment.BottomLeft, ContentAlignment.BottomRight], ]; for (int i = 0; i < 3; ++i) { if (mapping[i][0] == align) { return mapping[i][1]; } else if (mapping[i][1] == align) { return mapping[i][0]; } } } return align; } private int FullCheckSize => CheckSize + CheckPaddingSize; private void CalcCheckmarkRectangle(LayoutData layout) { int checkSizeFull = FullCheckSize; layout.CheckBounds = new Rectangle(Client.X, Client.Y, checkSizeFull, checkSizeFull); // Translate checkAlign for Rtl applications ContentAlignment align = RtlTranslateContent(CheckAlign); Rectangle field = Rectangle.Inflate(layout.Face, -PaddingSize, -PaddingSize); layout.Field = field; if (checkSizeFull <= 0) { return; } if ((align & LayoutUtils.AnyRight) != 0) { layout.CheckBounds.X = (field.X + field.Width) - layout.CheckBounds.Width; } else if ((align & LayoutUtils.AnyCenter) != 0) { layout.CheckBounds.X = field.X + (field.Width - layout.CheckBounds.Width) / 2; } if ((align & LayoutUtils.AnyBottom) != 0) { layout.CheckBounds.Y = (field.Y + field.Height) - layout.CheckBounds.Height; } else if ((align & LayoutUtils.AnyTop) != 0) { layout.CheckBounds.Y = field.Y + 2; // + 2: this needs to be aligned to the text ( } else { layout.CheckBounds.Y = field.Y + (field.Height - layout.CheckBounds.Height) / 2; } switch (align) { case ContentAlignment.TopLeft: case ContentAlignment.MiddleLeft: case ContentAlignment.BottomLeft: layout.CheckArea.X = field.X; layout.CheckArea.Width = checkSizeFull + 1; layout.CheckArea.Y = field.Y; layout.CheckArea.Height = field.Height; layout.Field.X += checkSizeFull + 1; layout.Field.Width -= checkSizeFull + 1; break; case ContentAlignment.TopRight: case ContentAlignment.MiddleRight: case ContentAlignment.BottomRight: layout.CheckArea.X = field.X + field.Width - checkSizeFull; layout.CheckArea.Width = checkSizeFull + 1; layout.CheckArea.Y = field.Y; layout.CheckArea.Height = field.Height; layout.Field.Width -= checkSizeFull + 1; break; case ContentAlignment.TopCenter: layout.CheckArea.X = field.X; layout.CheckArea.Width = field.Width; layout.CheckArea.Y = field.Y; layout.CheckArea.Height = checkSizeFull; layout.Field.Y += checkSizeFull; layout.Field.Height -= checkSizeFull; break; case ContentAlignment.BottomCenter: layout.CheckArea.X = field.X; layout.CheckArea.Width = field.Width; layout.CheckArea.Y = field.Y + field.Height - checkSizeFull; layout.CheckArea.Height = checkSizeFull; layout.Field.Height -= checkSizeFull; break; case ContentAlignment.MiddleCenter: layout.CheckArea = layout.CheckBounds; break; } layout.CheckBounds.Width -= CheckPaddingSize; layout.CheckBounds.Height -= CheckPaddingSize; } /// <summary> /// Maps an image align to the set of <see cref="Forms.TextImageRelation"/>s that represent the same edge. /// For example, <see cref="ContentAlignment.TopLeft"/> maps to <see cref="TextImageRelation.ImageAboveText"/> /// and <see cref="TextImageRelation.ImageBeforeText"/>. /// </summary> private static readonly TextImageRelation[] s_imageAlignToRelation = [ TextImageRelation.ImageAboveText | TextImageRelation.ImageBeforeText, // TopLeft TextImageRelation.ImageAboveText, // TopCenter TextImageRelation.ImageAboveText | TextImageRelation.TextBeforeImage, // TopRight 0, // Invalid TextImageRelation.ImageBeforeText, // MiddleLeft 0, // MiddleCenter TextImageRelation.TextBeforeImage, // MiddleRight 0, // Invalid TextImageRelation.TextAboveImage | TextImageRelation.ImageBeforeText, // BottomLeft TextImageRelation.TextAboveImage, // BottomCenter TextImageRelation.TextAboveImage | TextImageRelation.TextBeforeImage // BottomRight ]; private static TextImageRelation ImageAlignToRelation(ContentAlignment alignment) => s_imageAlignToRelation[LayoutUtils.ContentAlignmentToIndex(alignment)]; private static TextImageRelation TextAlignToRelation(ContentAlignment alignment) => LayoutUtils.GetOppositeTextImageRelation(ImageAlignToRelation(alignment)); internal void LayoutTextAndImage(LayoutData layout) { // Translate for Rtl applications. This shadows the member variables. ContentAlignment imageAlign = RtlTranslateContent(ImageAlign); ContentAlignment textAlign = RtlTranslateContent(TextAlign); TextImageRelation textImageRelation = RtlTranslateRelation(TextImageRelation); // Figure out the maximum bounds for text & image. Rectangle maxBounds = Rectangle.Inflate(layout.Field, -TextImageInset, -TextImageInset); if (OnePixExtraBorder) { maxBounds.Inflate(1, 1); } // Compute the final image and text bounds. if (ImageSize == Size.Empty || Text is null || Text.Length == 0 || textImageRelation == TextImageRelation.Overlay) { // Do not worry about text/image overlaying Size textSize = GetTextSize(maxBounds.Size); // For .NET Framework 1.1 compatibility. Size size = ImageSize; if (layout.Options.DotNetOneButtonCompat && ImageSize != Size.Empty) { size = new Size(size.Width + 1, size.Height + 1); } layout.ImageBounds = LayoutUtils.Align(size, maxBounds, imageAlign); layout.TextBounds = LayoutUtils.Align(textSize, maxBounds, textAlign); } else { // Rearrange text/image to prevent overlay. Pack text into maxBounds - space reserved for image. Size maxTextSize = LayoutUtils.SubAlignedRegion(maxBounds.Size, ImageSize, textImageRelation); Size textSize = GetTextSize(maxTextSize); Rectangle maxCombinedBounds = maxBounds; // Combine text & image into one rectangle that we center within maxBounds. Size combinedSize = LayoutUtils.AddAlignedRegion(textSize, ImageSize, textImageRelation); maxCombinedBounds.Size = LayoutUtils.UnionSizes(maxCombinedBounds.Size, combinedSize); Rectangle combinedBounds = LayoutUtils.Align(combinedSize, maxCombinedBounds, ContentAlignment.MiddleCenter); // ImageEdge indicates whether the combination of ImageAlign and TextImageRelation place // the image along the edge of the control. If so, we can increase the space for text. bool imageEdge = (AnchorStyles)(ImageAlignToRelation(imageAlign) & textImageRelation) != AnchorStyles.None; // TextEdge indicates whether the combination of TextAlign and TextImageRelation place // the text along the edge of the control. If so, we can increase the space for image. bool textEdge = (AnchorStyles)(TextAlignToRelation(textAlign) & textImageRelation) != AnchorStyles.None; if (imageEdge) { // Just split imageSize off of maxCombinedBounds. LayoutUtils.SplitRegion( maxCombinedBounds, ImageSize, (AnchorStyles)textImageRelation, out layout.ImageBounds, out layout.TextBounds); } else if (textEdge) { // Just split textSize off of maxCombinedBounds. LayoutUtils.SplitRegion( maxCombinedBounds, textSize, (AnchorStyles)LayoutUtils.GetOppositeTextImageRelation(textImageRelation), out layout.TextBounds, out layout.ImageBounds); } else { // Expand the adjacent regions to maxCombinedBounds (centered) and split the rectangle into // imageBounds and textBounds. LayoutUtils.SplitRegion( combinedBounds, ImageSize, (AnchorStyles)textImageRelation, out layout.ImageBounds, out layout.TextBounds); LayoutUtils.ExpandRegionsToFillBounds( maxCombinedBounds, (AnchorStyles)textImageRelation, ref layout.ImageBounds, ref layout.TextBounds); } // Align text/image within their regions. layout.ImageBounds = LayoutUtils.Align(ImageSize, layout.ImageBounds, imageAlign); layout.TextBounds = LayoutUtils.Align(textSize, layout.TextBounds, textAlign); } // Don't call "layout.imageBounds = Rectangle.Intersect(layout.imageBounds, maxBounds);" // because that is a breaking change that causes images to be scaled to the dimensions of the control. // adjust textBounds so that the text is still visible even if the image is larger than the button's size // Why do we intersect with layout.field for textBounds while we intersect with maxBounds for imageBounds? // this is because there are some legacy code which squeezes the button so small that text will get clipped // if we intersect with maxBounds. Have to do this for backward compatibility. if (textImageRelation is TextImageRelation.TextBeforeImage or TextImageRelation.ImageBeforeText) { // Adjust the vertical position of textBounds so that the text doesn't fall off the boundary of the button int textBottom = Math.Min(layout.TextBounds.Bottom, layout.Field.Bottom); layout.TextBounds.Y = Math.Max( Math.Min(layout.TextBounds.Y, layout.Field.Y + (layout.Field.Height - layout.TextBounds.Height) / 2), layout.Field.Y); layout.TextBounds.Height = textBottom - layout.TextBounds.Y; } if (textImageRelation is TextImageRelation.TextAboveImage or TextImageRelation.ImageAboveText) { // Adjust the horizontal position of textBounds so that the text doesn't fall off the boundary of the button int textRight = Math.Min(layout.TextBounds.Right, layout.Field.Right); layout.TextBounds.X = Math.Max( Math.Min(layout.TextBounds.X, layout.Field.X + (layout.Field.Width - layout.TextBounds.Width) / 2), layout.Field.X); layout.TextBounds.Width = textRight - layout.TextBounds.X; } if (textImageRelation == TextImageRelation.ImageBeforeText && layout.ImageBounds.Size.Width != 0) { // Squeezes imageBounds.Width so that text is visible layout.ImageBounds.Width = Math.Max( 0, Math.Min(maxBounds.Width - layout.TextBounds.Width, layout.ImageBounds.Width)); layout.TextBounds.X = layout.ImageBounds.X + layout.ImageBounds.Width; } if (textImageRelation == TextImageRelation.ImageAboveText && layout.ImageBounds.Size.Height != 0) { // Squeezes imageBounds.Height so that the text is visible layout.ImageBounds.Height = Math.Max( 0, Math.Min(maxBounds.Height - layout.TextBounds.Height, layout.ImageBounds.Height)); layout.TextBounds.Y = layout.ImageBounds.Y + layout.ImageBounds.Height; } // Make sure that textBound is contained in layout.field layout.TextBounds = Rectangle.Intersect(layout.TextBounds, layout.Field); if (HintTextUp) { layout.TextBounds.Y--; } if (TextOffset) { layout.TextBounds.Offset(1, 1); } // For .NET Framework 1.1 compatibility. if (layout.Options.DotNetOneButtonCompat) { layout.ImageStart = layout.ImageBounds.Location; layout.ImageBounds = Rectangle.Intersect(layout.ImageBounds, layout.Field); } else if (!Application.RenderWithVisualStyles) { // Not sure why this is here, but we can't remove it, since it might break // ToolStrips on non-themed machines layout.TextBounds.X++; } // Clip int bottom; // If we are using GDI to measure text, then we can get into a situation, where // the proposed height is ignore. In this case, we want to clip it against maxBounds. if (!UseCompatibleTextRendering) { bottom = Math.Min(layout.TextBounds.Bottom, maxBounds.Bottom); layout.TextBounds.Y = Math.Max(layout.TextBounds.Y, maxBounds.Y); } else { // If we are using GDI+ (like .NET Framework 1.1), then use the old code. // This ensures that we have pixel-level rendering compatibility. bottom = Math.Min(layout.TextBounds.Bottom, layout.Field.Bottom); layout.TextBounds.Y = Math.Max(layout.TextBounds.Y, layout.Field.Y); } layout.TextBounds.Height = bottom - layout.TextBounds.Y; } protected virtual Size GetTextSize(Size proposedSize) { // Set the Prefix field of TextFormatFlags proposedSize = LayoutUtils.FlipSizeIf(VerticalText, proposedSize); Size textSize = Size.Empty; if (UseCompatibleTextRendering) { // GDI+ text rendering. using var screen = GdiCache.GetScreenDCGraphics(); using StringFormat stringFormat = StringFormat; textSize = Size.Ceiling( screen.Graphics.MeasureString(Text, Font, new SizeF(proposedSize.Width, proposedSize.Height), stringFormat)); } else if (!string.IsNullOrEmpty(Text)) { // GDI text rendering (.NET Framework 2.0 feature). textSize = TextRenderer.MeasureText(Text, Font, proposedSize, TextFormatFlags); } // Else skip calling MeasureText, it should return 0,0 return LayoutUtils.FlipSizeIf(VerticalText, textSize); } #if DEBUG public override string ToString() => $$""" { client = {{Client}} OnePixExtraBorder = {{OnePixExtraBorder}} borderSize = {{BorderSize}} paddingSize = {{PaddingSize}} maxFocus = {{MaxFocus}} font = {{Font}} text = {{Text}} imageSize = {{ImageSize}} checkSize = {{CheckSize}} checkPaddingSize = {{CheckPaddingSize}} checkAlign = {{CheckAlign}} imageAlign = {{ImageAlign}} textAlign = {{TextAlign}} textOffset = {{TextOffset}} shadowedText = {{ShadowedText}} textImageRelation = {{TextImageRelation}} layoutRTL = {{LayoutRTL}} } """; #endif } } |