// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using MouseJump.Common.Models.Drawing; using MouseJump.Common.Models.Layout; using MouseJump.Common.Models.Styles; namespace MouseJump.Common.Helpers; public static class LayoutHelper { public static PreviewLayout GetPreviewLayout( PreviewStyle previewStyle, List screens, PointInfo activatedLocation) { ArgumentNullException.ThrowIfNull(previewStyle); ArgumentNullException.ThrowIfNull(screens); if (screens.Count == 0) { throw new ArgumentException("Value must contain at least one item.", nameof(screens)); } var builder = new PreviewLayout.Builder(); builder.Screens = screens.ToList(); // calculate the bounding rectangle for the virtual screen builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens); // find the screen that contains the activated location - this is the // one we'll show the preview form on var activatedScreen = builder.Screens.Single( screen => screen.Contains(activatedLocation)); builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen); // work out the maximum allowed size of the preview form: // * can't be bigger than the activated screen // * can't be bigger than the configured canvas size var maxPreviewSize = activatedScreen.Size .Intersect(previewStyle.CanvasSize); // the "content area" (i.e. drawing area) for screenshots is inside the // preview border and inside the preview padding (if any) var maxContentSize = maxPreviewSize .Shrink(previewStyle.CanvasStyle.MarginStyle) .Shrink(previewStyle.CanvasStyle.BorderStyle) .Shrink(previewStyle.CanvasStyle.PaddingStyle); // work out the actual size of the "content area" by scaling the virtual screen // to fit inside the maximum content area while maintaining its aspect ration. // we'll also offset it to allow for any margins, borders and padding var contentBounds = builder.VirtualScreen.Size .ScaleToFit(maxContentSize, out var scalingRatio) .Round() .Clamp(maxContentSize) .PlaceAt(0, 0) .Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top) .Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top) .Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top); // now we know the actual size of the content area we can work outwards to // get the size of the background bounds including margins, borders and padding builder.PreviewStyle = previewStyle; builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds( contentBounds, previewStyle.CanvasStyle); // ... and then the size and position of the preview form on the activated screen // * center the form to the activated position, but nudge it back // inside the visible area of the activated screen if it falls outside var formBounds = builder.PreviewBounds.OuterBounds .Center(activatedLocation) .Clamp(activatedScreen); builder.FormBounds = formBounds; // now calculate the positions of each of the screenshot images on the preview builder.ScreenshotBounds = builder.Screens .Select( screen => LayoutHelper.GetBoxBoundsFromOuterBounds( screen .Offset(builder.VirtualScreen.Location.ToSize().Invert()) .Scale(scalingRatio) .Offset(builder.PreviewBounds.ContentBounds.Location.ToSize()) .Round(), previewStyle.ScreenStyle)) .ToList(); return builder.Build(); } public static RectangleInfo GetCombinedScreenBounds(List screens) { return screens.Skip(1).Aggregate( seed: screens.First(), (bounds, screen) => bounds.Union(screen)); } /// /// Calculates the bounds of the various areas of a box, given the content bounds and the box style. /// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box. /// /// The content bounds of the box. /// The style of the box, which includes the sizes of the margin, border, and padding areas. /// A object that represents the bounds of the different areas of the box. /// Thrown when or is null. /// Thrown when any of the styles in is null. public static BoxBounds GetBoxBoundsFromContentBounds( RectangleInfo contentBounds, BoxStyle boxStyle) { ArgumentNullException.ThrowIfNull(contentBounds); ArgumentNullException.ThrowIfNull(boxStyle); if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null) { throw new ArgumentException(null, nameof(boxStyle)); } var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle); var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle); var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle); var outerBounds = marginBounds; return new( outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); } /// /// Calculates the bounds of the various areas of a box, given the outer bounds and the box style. /// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box. /// /// The outer bounds of the box. /// The style of the box, which includes the sizes of the margin, border, and padding areas. /// A object that represents the bounds of the different areas of the box. /// Thrown when or is null. /// Thrown when any of the styles in is null. public static BoxBounds GetBoxBoundsFromOuterBounds( RectangleInfo outerBounds, BoxStyle boxStyle) { ArgumentNullException.ThrowIfNull(outerBounds); ArgumentNullException.ThrowIfNull(boxStyle); if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null) { throw new ArgumentException(null, nameof(boxStyle)); } var marginBounds = outerBounds; var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle); var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle); var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle); return new( outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds); } }