diff --git a/Origami/SpinnerUtil.cs b/Origami/SpinnerUtil.cs index 404f106..6d0dd92 100644 --- a/Origami/SpinnerUtil.cs +++ b/Origami/SpinnerUtil.cs @@ -1,5 +1,6 @@ using System.Drawing; +using Prowl.PaperUI.LayoutEngine; using Prowl.Quill; using Prowl.Vector; @@ -30,15 +31,15 @@ public static class SpinnerUtil /// public struct SpinnerConfig { - /// Size of the spinner in pixels. - public double Size { get; set; } - + /// Size of the spinner. + public AbsoluteUnit Size { get; set; } + /// Color of the spinner. public Color Color { get; set; } - + /// Width of the spinner stroke. - public double StrokeWidth { get; set; } - + public AbsoluteUnit StrokeWidth { get; set; } + /// Animation speed multiplier (1.0 = normal speed). public double Speed { get; set; } @@ -77,20 +78,20 @@ public struct SpinnerConfig /// Paper UI instance /// Spinner configuration /// Action that can be used with AddActionElement - public static Action CreateSpinner(Paper paper, SpinnerConfig config) + public static Action CreateSpinner(Paper paper, SpinnerConfig config) { - return (canvas, rect) => { + return (canvas, rect, scalingSettings) => { var centerX = rect.x + rect.width / 2; var centerY = rect.y + rect.height / 2; - var radius = config.Size / 2; - + var radius = config.Size.ToPx(scalingSettings) / 2; + // Calculate rotation based on time var time = paper.Time; var rotation = (time * config.Speed * 2) % (Math.PI * 2); // Full rotation every second at speed 1.0 var rotDegrees = MathD.ToDeg(rotation); canvas.SaveState(); - + // Move to center and rotate canvas.TransformBy(Transform2D.CreateTranslation(centerX, centerY)); canvas.TransformBy(Transform2D.CreateRotate(rotDegrees)); @@ -99,7 +100,7 @@ public static Action CreateSpinner(Paper paper, SpinnerConfig conf canvas.BeginPath(); canvas.Arc(0, 0, radius, 0, Math.PI * 1.5); // 3/4 circle canvas.SetStrokeColor(config.Color); - canvas.SetStrokeWidth(config.StrokeWidth); + canvas.SetStrokeWidth(config.StrokeWidth.ToPx(scalingSettings)); canvas.Stroke(); canvas.RestoreState(); @@ -113,7 +114,7 @@ public static Action CreateSpinner(Paper paper, SpinnerConfig conf /// Origami theme for color consistency /// Size variant /// Action that can be used with AddActionElement - public static Action CreateThemedSpinner(Paper paper, OrigamiTheme theme, OrigamiSize size = OrigamiSize.Medium) + public static Action CreateThemedSpinner(Paper paper, OrigamiTheme theme, OrigamiSize size = OrigamiSize.Medium) { var config = size switch { @@ -135,7 +136,7 @@ public static Action CreateThemedSpinner(Paper paper, OrigamiTheme /// Color for the spinner /// Size variant /// Action that can be used with AddActionElement - public static Action CreateColoredSpinner(Paper paper, Color color, OrigamiSize size = OrigamiSize.Medium) + public static Action CreateColoredSpinner(Paper paper, Color color, OrigamiSize size = OrigamiSize.Medium) { var config = size switch { @@ -154,16 +155,16 @@ public static Action CreateColoredSpinner(Paper paper, Color color /// Paper UI instance /// Spinner configuration /// Action that can be used with AddActionElement - public static Action CreateDotsSpinner(Paper paper, SpinnerConfig config) + public static Action CreateDotsSpinner(Paper paper, SpinnerConfig config) { - return (canvas, rect) => { + return (canvas, rect, scalingSettings) => { var centerX = rect.x + rect.width / 2; var centerY = rect.y + rect.height / 2; var time = paper.Time * config.Speed; - + // Three dots with staggered animation - var dotSize = config.Size / 6; - var spacing = config.Size / 3; + var dotSize = config.Size.ToPx(scalingSettings) / 6; + var spacing = config.Size.ToPx(scalingSettings) / 3; canvas.SaveState(); @@ -171,12 +172,12 @@ public static Action CreateDotsSpinner(Paper paper, SpinnerConfig { var x = centerX + (i - 1) * spacing; var y = centerY; - + // Animate opacity based on time and dot index var animationOffset = i * 0.3; // Stagger the animation var opacity = (Math.Sin(time * 3 + animationOffset) + 1) / 2; // 0 to 1 opacity = Math.Max(0.3, opacity); // Minimum visibility - + var dotColor = Color.FromArgb((int)(255 * opacity), config.Color); canvas.BeginPath(); @@ -195,23 +196,23 @@ public static Action CreateDotsSpinner(Paper paper, SpinnerConfig /// Paper UI instance /// Spinner configuration /// Action that can be used with AddActionElement - public static Action CreatePulseSpinner(Paper paper, SpinnerConfig config) + public static Action CreatePulseSpinner(Paper paper, SpinnerConfig config) { - return (canvas, rect) => { + return (canvas, rect, scalingSettings) => { var centerX = rect.x + rect.width / 2; var centerY = rect.y + rect.height / 2; var time = paper.Time * config.Speed; - + // Pulsing radius - var baseRadius = config.Size / 3; + var baseRadius = config.Size.ToPx(scalingSettings) / 3; var pulseRadius = baseRadius + (Math.Sin(time * 4) + 1) / 2 * baseRadius * 0.5; - + // Pulsing opacity var opacity = (Math.Sin(time * 4) + 1) / 2 * 0.7 + 0.3; // 0.3 to 1.0 var pulseColor = Color.FromArgb((int)(255 * opacity), config.Color); canvas.SaveState(); - + canvas.BeginPath(); canvas.Circle(centerX, centerY, pulseRadius); canvas.SetFillColor(pulseColor); diff --git a/Paper/BoxShadow.cs b/Paper/BoxShadow.cs index d0bb1ef..731c867 100644 --- a/Paper/BoxShadow.cs +++ b/Paper/BoxShadow.cs @@ -1,4 +1,5 @@ using System.Drawing; +using Prowl.PaperUI.LayoutEngine; namespace Prowl.PaperUI { @@ -7,13 +8,13 @@ namespace Prowl.PaperUI /// public struct BoxShadow { - public double OffsetX; - public double OffsetY; - public double Blur; - public double Spread; + public AbsoluteUnit OffsetX; + public AbsoluteUnit OffsetY; + public AbsoluteUnit Blur; + public AbsoluteUnit Spread; public Color Color; - public BoxShadow(double offsetX, double offsetY, double blur, double spread, Color color) + public BoxShadow(AbsoluteUnit offsetX, AbsoluteUnit offsetY, AbsoluteUnit blur, AbsoluteUnit spread, Color color) { OffsetX = offsetX; OffsetY = offsetY; @@ -43,10 +44,10 @@ Color LerpColor(Color a, Color b) } return new BoxShadow( - start.OffsetX + (end.OffsetX - start.OffsetX) * t, - start.OffsetY + (end.OffsetY - start.OffsetY) * t, - start.Blur + (end.Blur - start.Blur) * t, - start.Spread + (end.Spread - start.Spread) * t, + AbsoluteUnit.Lerp(start.OffsetX, end.OffsetX, t), + AbsoluteUnit.Lerp(start.OffsetY, end.OffsetY, t), + AbsoluteUnit.Lerp(start.Blur, end.Blur, t), + AbsoluteUnit.Lerp(start.Spread, end.Spread, t), LerpColor(start.Color, end.Color) ); } diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index b06f6b7..1b5191f 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -52,10 +52,10 @@ public T BackgroundBoxGradient(double centerX, double centerY, double width, dou public T BorderColor(Color color) => SetStyleProperty(GuiProp.BorderColor, color); /// Sets the border width of the element. - public T BorderWidth(double width) => SetStyleProperty(GuiProp.BorderWidth, width); + public T BorderWidth(in AbsoluteUnit width) => SetStyleProperty(GuiProp.BorderWidth, width); /// Sets a box shadow on the element. - public T BoxShadow(double offsetX, double offsetY, double blur, double spread, Color color) => + public T BoxShadow(in AbsoluteUnit offsetX, in AbsoluteUnit offsetY, in AbsoluteUnit blur, in AbsoluteUnit spread, Color color) => SetStyleProperty(GuiProp.BoxShadow, new BoxShadow(offsetX, offsetY, blur, spread, color)); /// Sets a box shadow on the element. @@ -66,27 +66,27 @@ public T BoxShadow(double offsetX, double offsetY, double blur, double spread, C #region Corner Rounding /// Rounds the top corners of the element. - public T RoundedTop(double radius) => SetStyleProperty(GuiProp.Rounded, new Vector4(radius, radius, 0, 0)); + public T RoundedTop(in AbsoluteUnit radius) => SetStyleProperty(GuiProp.Rounded, new Rounding(radius, radius, 0, 0)); /// Rounds the bottom corners of the element. - public T RoundedBottom(double radius) => SetStyleProperty(GuiProp.Rounded, new Vector4(0, 0, radius, radius)); + public T RoundedBottom(in AbsoluteUnit radius) => SetStyleProperty(GuiProp.Rounded, new Rounding(0, 0, radius, radius)); /// Rounds the left corners of the element. - public T RoundedLeft(double radius) => SetStyleProperty(GuiProp.Rounded, new Vector4(radius, 0, 0, radius)); + public T RoundedLeft(in AbsoluteUnit radius) => SetStyleProperty(GuiProp.Rounded, new Rounding(radius, 0, 0, radius)); /// Rounds the right corners of the element. - public T RoundedRight(double radius) => SetStyleProperty(GuiProp.Rounded, new Vector4(0, radius, radius, 0)); + public T RoundedRight(in AbsoluteUnit radius) => SetStyleProperty(GuiProp.Rounded, new Rounding(0, radius, radius, 0)); /// Rounds all corners of the element with the same radius. - public T Rounded(double radius) => SetStyleProperty(GuiProp.Rounded, new Vector4(radius, radius, radius, radius)); + public T Rounded(in AbsoluteUnit radius) => SetStyleProperty(GuiProp.Rounded, new Rounding(radius, radius, radius, radius)); /// Rounds each corner of the element with individual radii. /// Top-left radius /// Top-right radius /// Bottom-right radius /// Bottom-left radius - public T Rounded(double tlRadius, double trRadius, double brRadius, double blRadius) => - SetStyleProperty(GuiProp.Rounded, new Vector4(tlRadius, trRadius, brRadius, blRadius)); + public T Rounded(in AbsoluteUnit tlRadius, in AbsoluteUnit trRadius, in AbsoluteUnit brRadius, in AbsoluteUnit blRadius) => + SetStyleProperty(GuiProp.Rounded, new Rounding(tlRadius, trRadius, brRadius, blRadius)); #endregion @@ -96,85 +96,85 @@ public T Rounded(double tlRadius, double trRadius, double brRadius, double blRad public T AspectRatio(double ratio) => SetStyleProperty(GuiProp.AspectRatio, ratio); /// Sets both width and height to the same value. - public T Size(in UnitValue sizeUniform) => Size(sizeUniform, sizeUniform); + public T Size(in RelativeUnit sizeUniform) => Size(sizeUniform, sizeUniform); /// Sets the width and height of the element. - public T Size(in UnitValue width, in UnitValue height) + public T Size(in RelativeUnit width, in RelativeUnit height) { SetStyleProperty(GuiProp.Width, width); return SetStyleProperty(GuiProp.Height, height); } /// Sets the width of the element. - public T Width(in UnitValue width) => SetStyleProperty(GuiProp.Width, width); + public T Width(in RelativeUnit width) => SetStyleProperty(GuiProp.Width, width); /// Sets the height of the element. - public T Height(in UnitValue height) => SetStyleProperty(GuiProp.Height, height); + public T Height(in RelativeUnit height) => SetStyleProperty(GuiProp.Height, height); /// Sets the minimum width of the element. - public T MinWidth(in UnitValue minWidth) => SetStyleProperty(GuiProp.MinWidth, minWidth); + public T MinWidth(in RelativeUnit minWidth) => SetStyleProperty(GuiProp.MinWidth, minWidth); /// Sets the maximum width of the element. - public T MaxWidth(in UnitValue maxWidth) => SetStyleProperty(GuiProp.MaxWidth, maxWidth); + public T MaxWidth(in RelativeUnit maxWidth) => SetStyleProperty(GuiProp.MaxWidth, maxWidth); /// Sets the minimum height of the element. - public T MinHeight(in UnitValue minHeight) => SetStyleProperty(GuiProp.MinHeight, minHeight); + public T MinHeight(in RelativeUnit minHeight) => SetStyleProperty(GuiProp.MinHeight, minHeight); /// Sets the maximum height of the element. - public T MaxHeight(in UnitValue maxHeight) => SetStyleProperty(GuiProp.MaxHeight, maxHeight); + public T MaxHeight(in RelativeUnit maxHeight) => SetStyleProperty(GuiProp.MaxHeight, maxHeight); /// Sets the position of the element from the left and top edges. - public T Position(in UnitValue left, in UnitValue top) + public T Position(in RelativeUnit left, in RelativeUnit top) { SetStyleProperty(GuiProp.Left, left); return SetStyleProperty(GuiProp.Top, top); } /// Sets the left position of the element. - public T Left(in UnitValue left) => SetStyleProperty(GuiProp.Left, left); + public T Left(in RelativeUnit left) => SetStyleProperty(GuiProp.Left, left); /// Sets the right position of the element. - public T Right(in UnitValue right) => SetStyleProperty(GuiProp.Right, right); + public T Right(in RelativeUnit right) => SetStyleProperty(GuiProp.Right, right); /// Sets the top position of the element. - public T Top(in UnitValue top) => SetStyleProperty(GuiProp.Top, top); + public T Top(in RelativeUnit top) => SetStyleProperty(GuiProp.Top, top); /// Sets the bottom position of the element. - public T Bottom(in UnitValue bottom) => SetStyleProperty(GuiProp.Bottom, bottom); + public T Bottom(in RelativeUnit bottom) => SetStyleProperty(GuiProp.Bottom, bottom); /// Sets the minimum left position of the element. - public T MinLeft(in UnitValue minLeft) => SetStyleProperty(GuiProp.MinLeft, minLeft); + public T MinLeft(in RelativeUnit minLeft) => SetStyleProperty(GuiProp.MinLeft, minLeft); /// Sets the maximum left position of the element. - public T MaxLeft(in UnitValue maxLeft) => SetStyleProperty(GuiProp.MaxLeft, maxLeft); + public T MaxLeft(in RelativeUnit maxLeft) => SetStyleProperty(GuiProp.MaxLeft, maxLeft); /// Sets the minimum right position of the element. - public T MinRight(in UnitValue minRight) => SetStyleProperty(GuiProp.MinRight, minRight); + public T MinRight(in RelativeUnit minRight) => SetStyleProperty(GuiProp.MinRight, minRight); /// Sets the maximum right position of the element. - public T MaxRight(in UnitValue maxRight) => SetStyleProperty(GuiProp.MaxRight, maxRight); + public T MaxRight(in RelativeUnit maxRight) => SetStyleProperty(GuiProp.MaxRight, maxRight); /// Sets the minimum top position of the element. - public T MinTop(in UnitValue minTop) => SetStyleProperty(GuiProp.MinTop, minTop); + public T MinTop(in RelativeUnit minTop) => SetStyleProperty(GuiProp.MinTop, minTop); /// Sets the maximum top position of the element. - public T MaxTop(in UnitValue maxTop) => SetStyleProperty(GuiProp.MaxTop, maxTop); + public T MaxTop(in RelativeUnit maxTop) => SetStyleProperty(GuiProp.MaxTop, maxTop); /// Sets the minimum bottom position of the element. - public T MinBottom(in UnitValue minBottom) => SetStyleProperty(GuiProp.MinBottom, minBottom); + public T MinBottom(in RelativeUnit minBottom) => SetStyleProperty(GuiProp.MinBottom, minBottom); /// Sets the maximum bottom position of the element. - public T MaxBottom(in UnitValue maxBottom) => SetStyleProperty(GuiProp.MaxBottom, maxBottom); + public T MaxBottom(in RelativeUnit maxBottom) => SetStyleProperty(GuiProp.MaxBottom, maxBottom); /// Sets uniform margin on all sides. - public T Margin(in UnitValue all) => Margin(all, all, all, all); + public T Margin(in RelativeUnit all) => Margin(all, all, all, all); /// Sets horizontal and vertical margins. - public T Margin(in UnitValue horizontal, in UnitValue vertical) => + public T Margin(in RelativeUnit horizontal, in RelativeUnit vertical) => Margin(horizontal, horizontal, vertical, vertical); /// Sets individual margins for each side. - public T Margin(in UnitValue left, in UnitValue right, in UnitValue top, in UnitValue bottom) + public T Margin(in RelativeUnit left, in RelativeUnit right, in RelativeUnit top, in RelativeUnit bottom) { SetStyleProperty(GuiProp.Left, left); SetStyleProperty(GuiProp.Right, right); @@ -183,44 +183,44 @@ public T Margin(in UnitValue left, in UnitValue right, in UnitValue top, in Unit } /// Sets the left padding for child elements. - public T ChildLeft(in UnitValue childLeft) => SetStyleProperty(GuiProp.ChildLeft, childLeft); + public T ChildLeft(in RelativeUnit childLeft) => SetStyleProperty(GuiProp.ChildLeft, childLeft); /// Sets the right padding for child elements. - public T ChildRight(in UnitValue childRight) => SetStyleProperty(GuiProp.ChildRight, childRight); + public T ChildRight(in RelativeUnit childRight) => SetStyleProperty(GuiProp.ChildRight, childRight); /// Sets the top padding for child elements. - public T ChildTop(in UnitValue childTop) => SetStyleProperty(GuiProp.ChildTop, childTop); + public T ChildTop(in RelativeUnit childTop) => SetStyleProperty(GuiProp.ChildTop, childTop); /// Sets the bottom padding for child elements. - public T ChildBottom(in UnitValue childBottom) => SetStyleProperty(GuiProp.ChildBottom, childBottom); + public T ChildBottom(in RelativeUnit childBottom) => SetStyleProperty(GuiProp.ChildBottom, childBottom); /// Sets the spacing between rows in a container. - public T RowBetween(in UnitValue rowBetween) => SetStyleProperty(GuiProp.RowBetween, rowBetween); + public T RowBetween(in RelativeUnit rowBetween) => SetStyleProperty(GuiProp.RowBetween, rowBetween); /// Sets the spacing between columns in a container. - public T ColBetween(in UnitValue colBetween) => SetStyleProperty(GuiProp.ColBetween, colBetween); + public T ColBetween(in RelativeUnit colBetween) => SetStyleProperty(GuiProp.ColBetween, colBetween); /// Sets the left border width. - public T BorderLeft(in UnitValue borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); + public T BorderLeft(in RelativeUnit borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); /// Sets the right border width. - public T BorderRight(in UnitValue borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); + public T BorderRight(in RelativeUnit borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); /// Sets the top border width. - public T BorderTop(in UnitValue borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); + public T BorderTop(in RelativeUnit borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); /// Sets the bottom border width. - public T BorderBottom(in UnitValue borderBottom) => SetStyleProperty(GuiProp.BorderBottom, borderBottom); + public T BorderBottom(in RelativeUnit borderBottom) => SetStyleProperty(GuiProp.BorderBottom, borderBottom); /// Sets uniform border width on all sides. - public T Border(in UnitValue all) => Border(all, all, all, all); + public T Border(in RelativeUnit all) => Border(all, all, all, all); /// Sets horizontal and vertical border widths. - public T Border(in UnitValue horizontal, in UnitValue vertical) => + public T Border(in RelativeUnit horizontal, in RelativeUnit vertical) => Border(horizontal, horizontal, vertical, vertical); /// Sets individual border widths for each side. - public T Border(in UnitValue left, in UnitValue right, in UnitValue top, in UnitValue bottom) + public T Border(in RelativeUnit left, in RelativeUnit right, in RelativeUnit top, in RelativeUnit bottom) { SetStyleProperty(GuiProp.BorderLeft, left); SetStyleProperty(GuiProp.BorderRight, right); @@ -236,29 +236,29 @@ public T Border(in UnitValue left, in UnitValue right, in UnitValue top, in Unit public T TextColor(Color color) => SetStyleProperty(GuiProp.TextColor, color); /// Sets the spacing between words in text. - public T WordSpacing(double spacing) => SetStyleProperty(GuiProp.WordSpacing, spacing); + public T WordSpacing(in AbsoluteUnit spacing) => SetStyleProperty(GuiProp.WordSpacing, spacing); /// Sets the spacing between letters in text. - public T LetterSpacing(double spacing) => SetStyleProperty(GuiProp.LetterSpacing, spacing); + public T LetterSpacing(in AbsoluteUnit spacing) => SetStyleProperty(GuiProp.LetterSpacing, spacing); /// Sets the height of a line in text. public T LineHeight(double height) => SetStyleProperty(GuiProp.LineHeight, height); /// Sets the size of a Tab character in spaces. public T TabSize(int size) => SetStyleProperty(GuiProp.TabSize, size); /// Sets the size of text in pixels. - public T FontSize(double size) => SetStyleProperty(GuiProp.FontSize, size); + public T FontSize(in AbsoluteUnit size) => SetStyleProperty(GuiProp.FontSize, size); #endregion #region Transform Properties /// Sets horizontal translation. - public T TranslateX(double x) => SetStyleProperty(GuiProp.TranslateX, x); + public T TranslateX(in AbsoluteUnit x) => SetStyleProperty(GuiProp.TranslateX, x); /// Sets vertical translation. - public T TranslateY(double y) => SetStyleProperty(GuiProp.TranslateY, y); + public T TranslateY(in AbsoluteUnit y) => SetStyleProperty(GuiProp.TranslateY, y); /// Sets both horizontal and vertical translation. - public T Translate(double x, double y) + public T Translate(in AbsoluteUnit x, in AbsoluteUnit y) { SetStyleProperty(GuiProp.TranslateX, x); return SetStyleProperty(GuiProp.TranslateY, y); @@ -303,7 +303,12 @@ public T TransformOrigin(double x, double y) return SetStyleProperty(GuiProp.OriginY, y); } - /// Sets a complete transform matrix. + /// + /// Sets a complete transform matrix. + /// + /// + /// The transform matrix is not scaled automatically. Use to apply scaling manually. + /// public T Transform(Transform2D transform) => SetStyleProperty(GuiProp.Transform, transform); #endregion @@ -628,19 +633,19 @@ public ElementBuilder IsNotFocusable() public ElementBuilder HookToParent() { _handle.Data.IsHookedToParent = true; - + // Mark the parent as having hooked children for optimization ElementHandle parent = _handle.GetParentHandle(); if (parent.IsValid) { parent.Data.IsAHookedParent = true; } - + return this; } /// - /// Sets the tab index for keyboard navigation. + /// Sets the tab index for keyboard navigation. /// Elements with lower tab indices are focused first when pressing Tab. /// Use -1 to exclude from tab navigation (default). /// @@ -895,7 +900,7 @@ public ElementBuilder Visible(bool visible) /// var textSize = MeasureText("My content", font, fontSize); /// return (textSize.Width + padding * 2, textSize.Height + padding * 2); /// }) - /// + /// /// // Example: Aspect ratio sizing /// .ContentSizer((maxWidth, maxHeight) => { /// const double aspectRatio = 16.0 / 9.0; @@ -1081,8 +1086,8 @@ private void ConfigureScrollHandlers() Vector2 mousePos = _paper.PointerPos; // Check if pointer is over scrollbars - state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); - state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); + state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); + state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); _paper.SetElementStorage(_handle, "ScrollState", state); }); @@ -1103,17 +1108,20 @@ private void ConfigureScrollHandlers() if (state.IsDraggingVertical || state.IsDraggingHorizontal) return; + // Adjust scroll speed as needed + double scrollSpeed = UnitValue.Points(30).ToPx(_paper.ScalingSettings); + if ((_handle.Data.ScrollFlags & Scroll.ScrollY) != 0) { state.Position = new Vector2( state.Position.x, - state.Position.y - e.Delta * 30 // Adjust scroll speed as needed + state.Position.y - e.Delta * scrollSpeed ); } else if ((_handle.Data.ScrollFlags & Scroll.ScrollX) != 0) { state.Position = new Vector2( - state.Position.x - e.Delta * 30, + state.Position.x - e.Delta * scrollSpeed, state.Position.y ); } @@ -1131,8 +1139,8 @@ private void ConfigureScrollHandlers() Vector2 mousePos = _paper.PointerPos; // Check if click is on a scrollbar - bool onVertical = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); - bool onHorizontal = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); + bool onVertical = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); + bool onHorizontal = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); // Start dragging the appropriate scrollbar if (onVertical) @@ -1162,11 +1170,11 @@ private void ConfigureScrollHandlers() // Handle scrollbar dragging if (state.IsDraggingVertical) { - state.HandleVerticalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags); + state.HandleVerticalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); } else if (state.IsDraggingHorizontal) { - state.HandleHorizontalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags); + state.HandleHorizontalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); } _paper.SetElementStorage(_handle, "ScrollState", state); @@ -1183,8 +1191,8 @@ private void ConfigureScrollHandlers() // Update hover state on release Vector2 mousePos = _paper.PointerPos; - state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); - state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags); + state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); + state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); _paper.SetElementStorage(_handle, "ScrollState", state); }); @@ -1201,19 +1209,19 @@ public struct TextInputSettings { /// Font used to render the text public FontFile Font; - + /// Color of the text public Color TextColor; - + /// Placeholder text shown when the field is empty public string Placeholder; - + /// Color of the placeholder text public Color PlaceholderColor; - + /// Whether the input is read-only public bool ReadOnly; - + /// Maximum number of characters allowed (0 = no limit) public int MaxLength; @@ -1249,36 +1257,36 @@ private struct TextInputState public bool IsMultiLine; public readonly bool HasSelection => SelectionStart >= 0 && SelectionEnd >= 0 && SelectionStart != SelectionEnd; - + public void ClearSelection() { SelectionStart = -1; SelectionEnd = -1; } - + public void DeleteSelection() { if (!HasSelection) return; - + int start = Math.Min(SelectionStart, SelectionEnd); int end = Math.Max(SelectionStart, SelectionEnd); Value = Value.Remove(start, end - start); CursorPosition = start; ClearSelection(); } - + public void ClampValues() { CursorPosition = Math.Clamp(CursorPosition, 0, Value.Length); SelectionStart = SelectionStart < 0 ? -1 : Math.Clamp(SelectionStart, 0, Value.Length); SelectionEnd = SelectionEnd < 0 ? -1 : Math.Clamp(SelectionEnd, 0, Value.Length); } - + /// Gets the current line that contains the cursor public readonly int GetCursorLine() { if (!IsMultiLine || string.IsNullOrEmpty(Value)) return 0; - + int line = 0; for (int i = 0; i < CursorPosition && i < Value.Length; i++) { @@ -1286,30 +1294,30 @@ public readonly int GetCursorLine() } return line; } - + /// Gets all lines in the text public readonly string[] GetLines() { if (string.IsNullOrEmpty(Value)) return new[] { "" }; return Value.Split('\n'); } - + /// Gets the column position of the cursor within its line public readonly int GetCursorColumn() { if (string.IsNullOrEmpty(Value)) return 0; if (CursorPosition == 0) return 0; - + int lastNewline = Value.LastIndexOf('\n', Math.Min(CursorPosition - 1, Value.Length - 1)); return CursorPosition - (lastNewline + 1); } - + /// Clamps scroll offsets to valid ranges for text input public void ClampScrollOffsets(double contentWidth, double contentHeight, double visibleWidth, double visibleHeight) { double maxScrollX = Math.Max(0, contentWidth - visibleWidth); double maxScrollY = Math.Max(0, contentHeight - visibleHeight); - + ScrollOffsetX = Math.Clamp(ScrollOffsetX, 0, maxScrollX); ScrollOffsetY = Math.Clamp(ScrollOffsetY, 0, maxScrollY); } @@ -1331,23 +1339,23 @@ private TextInputState LoadTextInputState(string initialValue, bool isMultiLine) IsFocused = false, IsMultiLine = isMultiLine }; - + var state = _paper.GetElementStorage(_handle, "TextInputState", defaultState); state.IsFocused = _paper.IsElementFocused(_handle.Data.ID); state.IsMultiLine = isMultiLine; // Ensure consistency state.ClampValues(); return state; } - + private void SaveTextInputState(TextInputState state) { _paper.SetElementStorage(_handle, "TextInputState", state); } - + private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSettings, bool isMultiLine, double maxWidth = float.MaxValue) { - var fontSize = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); - var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); + var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper.ScalingSettings); + var letterSpacing = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing)).ToPx(_paper.ScalingSettings); var settings = TextLayoutSettings.Default; settings.PixelSize = (float)fontSize; @@ -1356,10 +1364,10 @@ private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSetti settings.Alignment = Scribe.TextAlignment.Left; settings.MaxWidth = (float)maxWidth; settings.WrapMode = (isMultiLine && inputSettings.DoWrap) ? Scribe.TextWrapMode.Wrap : TextWrapMode.NoWrap; - + return settings; } - + private bool IsShiftPressed() => _paper.IsKeyDown(PaperKey.LeftShift) || _paper.IsKeyDown(PaperKey.RightShift); private bool IsControlPressed() => _paper.IsKeyDown(PaperKey.LeftControl) || _paper.IsKeyDown(PaperKey.RightControl); @@ -1369,21 +1377,21 @@ private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSetti private int FindPreviousWordStart(string text, int position) { if (string.IsNullOrEmpty(text) || position <= 0) return 0; - + int pos = Math.Min(position - 1, text.Length - 1); - + // Skip whitespace while (pos > 0 && char.IsWhiteSpace(text[pos])) pos--; - + // Skip word characters while (pos > 0 && !char.IsWhiteSpace(text[pos])) pos--; - + // Move to start of word if we stopped at whitespace if (pos > 0 && char.IsWhiteSpace(text[pos])) pos++; - + return pos; } @@ -1393,17 +1401,17 @@ private int FindPreviousWordStart(string text, int position) private int FindNextWordEnd(string text, int position) { if (string.IsNullOrEmpty(text) || position >= text.Length) return text?.Length ?? 0; - + int pos = position; - + // Skip whitespace while (pos < text.Length && char.IsWhiteSpace(text[pos])) pos++; - + // Skip word characters while (pos < text.Length && !char.IsWhiteSpace(text[pos])) pos++; - + return pos; } @@ -1414,40 +1422,40 @@ private int FindNextWordEnd(string text, int position) { if (string.IsNullOrEmpty(text) || position < 0 || position >= text.Length) return (position, position); - + // If we're on whitespace, return the position as both start and end if (char.IsWhiteSpace(text[position])) return (position, position); - + int start = position; int end = position; - + // Find start of word while (start > 0 && !char.IsWhiteSpace(text[start - 1])) start--; - + // Find end of word while (end < text.Length && !char.IsWhiteSpace(text[end])) end++; - + return (start, end); } private void MoveCursorVertical(ref TextInputState state, int direction, TextInputSettings settings) { if (!state.IsMultiLine) return; - + var lines = state.GetLines(); int currentLine = state.GetCursorLine(); int targetLine = Math.Clamp(currentLine + direction, 0, lines.Length - 1); - + if (targetLine == currentLine) return; int currentColumn = state.GetCursorColumn(); // Move to the same column in the target line, or end of line if shorter int targetColumn = Math.Min(currentColumn, lines[targetLine].Length); - + // Calculate new cursor position int newPosition = 0; for (int i = 0; i < targetLine; i++) @@ -1458,7 +1466,7 @@ private void MoveCursorVertical(ref TextInputState state, int direction, TextInp newPosition += 1; } newPosition += targetColumn; - + if (IsShiftPressed()) { if (state.SelectionStart < 0) state.SelectionStart = state.CursorPosition; @@ -1475,7 +1483,7 @@ private void MoveCursorVertical(ref TextInputState state, int direction, TextInp private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInputSettings settings) { bool valueChanged = false; - + switch (key) { case PaperKey.Backspace: @@ -1491,7 +1499,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.Delete: if (state.HasSelection) { @@ -1504,7 +1512,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.Left: if (IsControlPressed()) { @@ -1537,7 +1545,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.Right: if (IsControlPressed()) { @@ -1570,7 +1578,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.Home: if (IsShiftPressed()) { @@ -1584,7 +1592,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.End: if (IsShiftPressed()) { @@ -1598,13 +1606,13 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.A when IsControlPressed(): state.SelectionStart = 0; state.SelectionEnd = state.Value.Length; state.CursorPosition = state.SelectionEnd; break; - + case PaperKey.C when IsControlPressed() && state.HasSelection: { int start = Math.Min(state.SelectionStart, state.SelectionEnd); @@ -1612,7 +1620,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput _paper.SetClipboard(state.Value.Substring(start, end - start)); } break; - + case PaperKey.X when IsControlPressed() && state.HasSelection: { int start = Math.Min(state.SelectionStart, state.SelectionEnd); @@ -1622,7 +1630,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.V when IsControlPressed(): { string clipText = _paper.GetClipboard(); @@ -1631,7 +1639,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput // For single-line, replace newlines with spaces if (!state.IsMultiLine) clipText = clipText.Replace('\n', ' ').Replace('\r', ' '); - + // Check max length if (settings.MaxLength > 0) { @@ -1644,7 +1652,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput if (availableLength > 0 && clipText.Length > availableLength) clipText = clipText.Substring(0, availableLength); } - + if (!string.IsNullOrEmpty(clipText)) { if (state.HasSelection) state.DeleteSelection(); @@ -1661,7 +1669,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput if (!settings.ReadOnly) { if (state.HasSelection) state.DeleteSelection(); - + // Check max length if (settings.MaxLength == 0 || state.Value.Length < settings.MaxLength) { @@ -1671,10 +1679,10 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput } } break; - + case PaperKey.Enter when state.IsMultiLine: if (state.HasSelection) state.DeleteSelection(); - + // Check max length // Check max length and read-only if (!settings.ReadOnly && (settings.MaxLength == 0 || state.Value.Length < settings.MaxLength)) @@ -1684,16 +1692,16 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.Up when state.IsMultiLine: MoveCursorVertical(ref state, -1, settings); break; - + case PaperKey.Down when state.IsMultiLine: MoveCursorVertical(ref state, 1, settings); break; } - + return valueChanged; } @@ -1740,7 +1748,7 @@ public ElementBuilder TextField( settings.TextColor = textColor ?? settings.TextColor; settings.Placeholder = placeholder; settings.PlaceholderColor = placeholderColor ?? settings.PlaceholderColor; - + return CreateTextInput(value, settings, onChange, false, intID); } @@ -1787,7 +1795,7 @@ public ElementBuilder TextArea( settings.TextColor = textColor ?? settings.TextColor; settings.Placeholder = placeholder; settings.PlaceholderColor = placeholderColor ?? settings.PlaceholderColor; - + return CreateTextInput(value, settings, onChange, true, intID); } @@ -1827,17 +1835,17 @@ private ElementBuilder CreateTextInput( { var currentState = LoadTextInputState(value, isMultiLine); currentState.IsFocused = e.IsFocused; - + if (e.IsFocused) { currentState.CursorPosition = currentState.Value.Length; currentState.ClearSelection(); EnsureCursorVisible(ref currentState, settings, isMultiLine); } - + SaveTextInputState(currentState); }); - + // Handle mouse clicks for cursor positioning and Shift+Click range selection OnPress((ClickEvent e) => { @@ -1865,7 +1873,7 @@ private ElementBuilder CreateTextInput( currentState.CursorPosition = newPosition; currentState.ClearSelection(); } - + EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); }); @@ -1891,7 +1899,7 @@ private ElementBuilder CreateTextInput( SaveTextInputState(currentState); } }); - + // Handle dragging for text selection OnDragStart((DragEvent e) => { @@ -1899,28 +1907,28 @@ private ElementBuilder CreateTextInput( var dragPos = e.RelativePosition.x + currentState.ScrollOffsetX; var dragPosY = isMultiLine ? e.RelativePosition.y + currentState.ScrollOffsetY : 0; var pos = Math.Clamp(CalculateTextPosition(currentState.Value, settings, isMultiLine, dragPos, dragPosY), 0, currentState.Value.Length); - + currentState.CursorPosition = pos; currentState.SelectionStart = pos; currentState.SelectionEnd = pos; EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); }); - + OnDragging((DragEvent e) => { var currentState = LoadTextInputState(value, isMultiLine); if (currentState.SelectionStart < 0) return; - + // Auto-scroll when dragging near edges - const double edgeScrollSensitivity = 20.0; - const double scrollSpeed = 2.0; - + double edgeScrollSensitivity = UnitValue.Points(20).ToPx(_paper.ScalingSettings); + double scrollSpeed = UnitValue.Points(2).ToPx(_paper.ScalingSettings); + if (e.RelativePosition.x < edgeScrollSensitivity) currentState.ScrollOffsetX = Math.Max(0, currentState.ScrollOffsetX - scrollSpeed); else if (e.RelativePosition.x > e.ElementRect.width - edgeScrollSensitivity) currentState.ScrollOffsetX += scrollSpeed; - + if (isMultiLine) { if (e.RelativePosition.y < edgeScrollSensitivity) @@ -1928,58 +1936,58 @@ private ElementBuilder CreateTextInput( else if (e.RelativePosition.y > e.ElementRect.height - edgeScrollSensitivity) currentState.ScrollOffsetY += scrollSpeed; } - + // Clamp scroll offsets after auto-scroll var layoutSettings = CreateTextLayoutSettings(settings, isMultiLine, e.ElementRect.width); var textLayout = _paper.CreateLayout(currentState.Value, layoutSettings); double visibleWidth = e.ElementRect.width; double visibleHeight = e.ElementRect.height; currentState.ClampScrollOffsets(textLayout.Size.X, textLayout.Size.Y, visibleWidth, visibleHeight); - + var dragPos = e.RelativePosition.x + currentState.ScrollOffsetX; var dragPosY = isMultiLine ? e.RelativePosition.y + currentState.ScrollOffsetY : 0; var pos = Math.Clamp(CalculateTextPosition(currentState.Value, settings, isMultiLine, dragPos, dragPosY), 0, currentState.Value.Length); - + currentState.CursorPosition = pos; currentState.SelectionEnd = pos; EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); }); - - // Handle keyboard input + + // Handle keyboard input OnKeyPressed((KeyEvent e) => { var currentState = LoadTextInputState(value, isMultiLine); if (!currentState.IsFocused) return; - + bool valueChanged = ProcessKeyCommand(ref currentState, e.Key, settings); - + EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); - + if (valueChanged) onChange?.Invoke(currentState.Value); }); - + // Handle character input OnTextInput((TextInputEvent e) => { var currentState = LoadTextInputState(value, isMultiLine); if (!currentState.IsFocused || char.IsControl(e.Character) || settings.ReadOnly) return; - + // Check max length if (settings.MaxLength > 0 && currentState.Value.Length >= settings.MaxLength && !currentState.HasSelection) return; - + if (currentState.HasSelection) currentState.DeleteSelection(); - + // For single-line, don't allow newlines if (!isMultiLine && (e.Character == '\n' || e.Character == '\r')) return; - + currentState.Value = currentState.Value.Insert(currentState.CursorPosition, e.Character.ToString()); currentState.CursorPosition++; - + EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); onChange?.Invoke(currentState.Value); @@ -1988,15 +1996,15 @@ private ElementBuilder CreateTextInput( // Render cursor and selection OnPostLayout((ElementHandle elHandle, Rect rect) => { - _paper.AddActionElement(ref elHandle, (canvas, r) => + _paper.AddActionElement(ref elHandle, (canvas, r, scalingSettings) => { var renderState = LoadTextInputState(value, isMultiLine); var layoutSettings = CreateTextLayoutSettings(settings, isMultiLine, r.width); - + canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); - var fontSize = (double)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize); + var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(scalingSettings); // Draw text or placeholder if (string.IsNullOrEmpty(renderState.Value)) @@ -2007,37 +2015,37 @@ private ElementBuilder CreateTextInput( { canvas.DrawText(renderState.Value, (float)(r.x), (float)r.y, settings.TextColor, layoutSettings); } - + // Draw selection and cursor if focused if (renderState.IsFocused) { _paper.CaptureKeyboard(); - + // Draw selection background if (renderState.HasSelection) { int start = Math.Min(renderState.SelectionStart, renderState.SelectionEnd); int end = Math.Max(renderState.SelectionStart, renderState.SelectionEnd); - + var textLayout = _paper.CreateLayout(renderState.Value, layoutSettings); var startPos = textLayout.GetCursorPosition(start); var endPos = textLayout.GetCursorPosition(end); - + canvas.SetFillColor(Color.FromArgb(100, 100, 150, 255)); - + if (isMultiLine && Math.Abs(endPos.Y - startPos.Y) > fontSize / 2) { // Multi-line selection: Draw rectangles for each line double lineHeight = fontSize * layoutSettings.LineHeight; double currentY = startPos.Y; - + // Get line indices from Y positions int startLineIndex = (int)(startPos.Y / lineHeight); int endLineIndex = (int)(endPos.Y / lineHeight); // First line: from start position to end of line double firstLineWidth = startLineIndex < textLayout.Lines.Count ? textLayout.Lines[startLineIndex].Width : 0; - + canvas.BeginPath(); canvas.RoundedRect( r.x + startPos.X, @@ -2046,14 +2054,14 @@ private ElementBuilder CreateTextInput( lineHeight, 2, 2, 2, 2); canvas.Fill(); - + // Middle lines: use actual line widths from textLayout currentY += lineHeight; int currentLineIndex = startLineIndex + 1; while (currentY < endPos.Y && currentLineIndex < textLayout.Lines.Count) { float lineWidth = textLayout.Lines[currentLineIndex].Width; - + canvas.BeginPath(); canvas.RoundedRect( r.x, @@ -2065,7 +2073,7 @@ private ElementBuilder CreateTextInput( currentY += lineHeight; currentLineIndex++; } - + // Last line: from start of line to end position if (endPos.X > 0) { @@ -2084,15 +2092,15 @@ private ElementBuilder CreateTextInput( // Single-line selection: Draw one rectangle canvas.BeginPath(); canvas.RoundedRect( - r.x + startPos.X, - r.y + startPos.Y, + r.x + startPos.X, + r.y + startPos.Y, endPos.X - startPos.X, - fontSize, + fontSize, 2, 2, 2, 2); canvas.Fill(); } } - + // Draw blinking cursor if ((int)(_paper.Time * 2) % 2 == 0) { @@ -2100,7 +2108,7 @@ private ElementBuilder CreateTextInput( var cursorPos = textLayout.GetCursorPosition(renderState.CursorPosition); double cursorX = r.x + cursorPos.X; double cursorY = r.y + cursorPos.Y; - + canvas.BeginPath(); canvas.MoveTo(cursorX, cursorY); canvas.LineTo(cursorX, cursorY + fontSize); @@ -2109,16 +2117,16 @@ private ElementBuilder CreateTextInput( canvas.Stroke(); } } - + canvas.RestoreState(); }); }); return this; } - + // Helper methods for text field functionality - + /// /// Ensures the cursor is visible by adjusting scroll position if needed. /// @@ -2129,18 +2137,18 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set // For multi-line, we need both horizontal and vertical scrolling var textLayout = _paper.CreateLayout(state.Value, CreateTextLayoutSettings(settings, true, _handle.Data.LayoutWidth)); var cursorPos = textLayout.GetCursorPosition(state.CursorPosition); - + double visibleWidth = _handle.Data.LayoutWidth; double visibleHeight = _handle.Data.LayoutHeight; - - const double margin = 10.0; - + + double margin = UnitValue.Points(10).ToPx(_paper.ScalingSettings); + // Horizontal scrolling if (cursorPos.X < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.X - margin); else if (cursorPos.X > state.ScrollOffsetX + visibleWidth - margin) state.ScrollOffsetX = cursorPos.X - visibleWidth + margin; - + // Vertical scrolling if (cursorPos.Y < state.ScrollOffsetY + margin) state.ScrollOffsetY = Math.Max(0, cursorPos.Y - margin); @@ -2153,13 +2161,13 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set else { // Single-line horizontal scrolling only - var fontSize = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); - var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); + var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper.ScalingSettings); + var letterSpacing = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing)).ToPx(_paper.ScalingSettings); var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); - + double visibleWidth = _handle.Data.LayoutWidth; - const double margin = 20.0; - + double margin = UnitValue.Points(20).ToPx(_paper.ScalingSettings); + if (cursorPos.x < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.x - margin); else if (cursorPos.x > state.ScrollOffsetX + visibleWidth - margin) @@ -2170,7 +2178,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set state.ClampScrollOffsets(textSize.x, textSize.y, visibleWidth, _handle.Data.LayoutHeight); } } - + /// /// Calculates the closest text position based on coordinates using TextLayout. /// @@ -2181,7 +2189,7 @@ private int CalculateTextPosition(string text, TextInputSettings settings, bool var textLayout = _paper.CreateLayout(text, CreateTextLayoutSettings(settings, isMultiLine, maxWidth)); return textLayout.GetCursorIndex(new Vector2(x, y)); } - + /// /// Calculates the cursor position for a specific character index using TextLayout. /// @@ -2196,7 +2204,7 @@ private Vector2 GetCursorPositionFromIndex(string text, FontFile font, double fo var textLayout = _paper.CreateLayout(text, settings); return textLayout.GetCursorPosition(index); } - + #endregion /// diff --git a/Paper/ElementRenderCommand.cs b/Paper/ElementRenderCommand.cs index 48a92f9..b3b98a2 100644 --- a/Paper/ElementRenderCommand.cs +++ b/Paper/ElementRenderCommand.cs @@ -1,4 +1,5 @@ -using Prowl.Quill; +using Prowl.PaperUI.LayoutEngine; +using Prowl.Quill; using Prowl.Vector; namespace Prowl.PaperUI @@ -9,6 +10,6 @@ namespace Prowl.PaperUI internal class ElementRenderCommand { public LayoutEngine.ElementHandle Element { get; set; } - public Action? RenderAction { get; set; } + public Action? RenderAction { get; set; } } } diff --git a/Paper/Enums.cs b/Paper/Enums.cs index 2af80c3..2c9bbd4 100644 --- a/Paper/Enums.cs +++ b/Paper/Enums.cs @@ -37,13 +37,32 @@ public enum PositionType /// /// Defines measurement units for element dimensions and positioning. /// - public enum Units + public enum AbsoluteUnits { /// - /// Fixed pixel measurements. + /// Fixed-sized unit with each pixel corresponding to a physical pixel on the screen. + /// Useful for precise element sizing. /// Pixels, + /// + /// Variable-sized unit based on the device's pixel density. + /// Useful for automatic element sizing based on the device's pixel density. + /// + Points + } + + /// + /// Defines measurement units for element dimensions and positioning. + /// + public enum RelativeUnits + { + /// + Pixels, + + /// + Points, + /// /// Percentage of parent container's corresponding dimension. /// diff --git a/Paper/LayoutEngine/AbsoluteUnit.cs b/Paper/LayoutEngine/AbsoluteUnit.cs new file mode 100644 index 0000000..d3bd61b --- /dev/null +++ b/Paper/LayoutEngine/AbsoluteUnit.cs @@ -0,0 +1,212 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +namespace Prowl.PaperUI.LayoutEngine +{ + /// + /// Represents a value with a unit type for UI layout measurements. + /// Supports pixels and points with interpolation capabilities. + /// + public struct AbsoluteUnit : IEquatable + { + /// + /// Helper class for interpolation between two AbsoluteUnit instances. + /// Using a simplified class approach to avoid struct cycles. + /// + private class LerpData + { + public readonly AbsoluteUnit Start; + public readonly AbsoluteUnit End; + public readonly double Progress; + + public LerpData(AbsoluteUnit start, AbsoluteUnit end, double progress) + { + Start = start; + End = end; + Progress = progress; + } + } + + /// The unit type of this value + public AbsoluteUnits Type { get; set; } = AbsoluteUnits.Points; + + /// The numeric value in the specified units + public double Value { get; set; } = 0f; + + /// Data for interpolation between two AbsoluteUnits (null when not interpolating) + private LerpData? _lerpData = null; + + /// + /// Creates a default AbsoluteUnit with Points units. + /// + public AbsoluteUnit() { } + + /// + /// Creates a AbsoluteUnit with the specified type and value. + /// + /// The unit type + /// The numeric value + public AbsoluteUnit(AbsoluteUnits type, double value = 0f) + { + Type = type; + Value = value; + } + + #region Type Checking Properties + + /// Returns true if this value is using Pixel units + public bool IsPixels => Type == AbsoluteUnits.Pixels; + + /// Returns true if this value is using Point units + public bool IsPoints => Type == AbsoluteUnits.Points; + + #endregion + + /// + /// Converts this unit value to pixels. + /// + /// Settings to use for scaling calculations + /// Size in pixels + public readonly double ToPx(in ScalingSettings scalingSettings) + { + // Handle interpolation if active + if (_lerpData != null) + { + var startPx = _lerpData.Start.ToPx(scalingSettings); + var endPx = _lerpData.End.ToPx(scalingSettings); + return startPx + (endPx - startPx) * _lerpData.Progress; + } + + return Type switch { + AbsoluteUnits.Pixels => Value, + AbsoluteUnits.Points => Value * scalingSettings.ContentScale, + _ => throw new ArgumentOutOfRangeException() + }; + } + + /// + /// Linearly interpolates between two AbsoluteUnit instances. + /// In reality, it creates a new AbsoluteUnit with special interpolation data which is calculated when ToPx is called. + /// + /// Starting value + /// Ending value + /// Interpolation factor (0.0 to 1.0) + /// Interpolated AbsoluteUnit + public static AbsoluteUnit Lerp(in AbsoluteUnit a, in AbsoluteUnit b, double blendFactor) + { + // Ensure blend factor is between 0 and 1 + blendFactor = Math.Clamp(blendFactor, 0f, 1f); + + // If units are the same, we can blend directly + if (a.Type == b.Type) + { + return new AbsoluteUnit( + a.Type, + a.Value + (b.Value - a.Value) * blendFactor + ); + } + + // If units are different, use interpolation data + var result = new AbsoluteUnit { + Type = a.Type, + Value = a.Value, + _lerpData = new LerpData(a, b, blendFactor) + }; + return result; + } + + #region Implicit Conversions + + /// + /// Implicitly converts an integer to a point unit AbsoluteUnit. + /// + public static implicit operator AbsoluteUnit(int value) + { + return new AbsoluteUnit(AbsoluteUnits.Points, value); + } + + /// + /// Implicitly converts a double to a point unit AbsoluteUnit. + /// + public static implicit operator AbsoluteUnit(double value) + { + return new AbsoluteUnit(AbsoluteUnits.Points, value); + } + + /// + /// Implicitly converts an AbsoluteUnit to a RelativeUnit. + /// + public static implicit operator RelativeUnit(AbsoluteUnit value) + { + var relativeUnitType = value.Type switch + { + AbsoluteUnits.Pixels => RelativeUnits.Pixels, + AbsoluteUnits.Points => RelativeUnits.Points, + _ => throw new ArgumentOutOfRangeException() + }; + + return new RelativeUnit(relativeUnitType, value.Value); + } + + #endregion + + #region Equality and Hashing + + public static bool operator ==(AbsoluteUnit left, AbsoluteUnit right) + { + return left.Equals(right); + } + + public static bool operator !=(AbsoluteUnit left, AbsoluteUnit right) + { + return !left.Equals(right); + } + + /// + /// Compares this AbsoluteUnit with another object for equality. + /// + public override readonly bool Equals(object? obj) + { + return obj is AbsoluteUnit other && Equals(other); + } + + public readonly bool Equals(AbsoluteUnit other) + { + // First, check the basic properties + bool basicPropertiesEqual = Type == other.Type && + Value.Equals(other.Value); + + // If either value isn't interpolating, they're equal only if both aren't + if (_lerpData is null || other._lerpData is null) + return basicPropertiesEqual && _lerpData is null && other._lerpData is null; + + // Both values are interpolating – compare their interpolation data safely + var thisLerp = _lerpData; + var otherLerp = other._lerpData; + bool lerpPropsEqual = thisLerp.Start.Equals(otherLerp.Start) && + thisLerp.End.Equals(otherLerp.End) && + thisLerp.Progress.Equals(otherLerp.Progress); + + return basicPropertiesEqual && lerpPropsEqual; + } + + /// + /// Returns a hash code for this AbsoluteUnit. + /// + public override readonly int GetHashCode() + { + return HashCode.Combine((int)Type, Value); + } + + #endregion + + /// + /// Returns a string representation of this AbsoluteUnit. + /// + public override readonly string ToString() => Type switch { + AbsoluteUnits.Pixels => $"{Value}px", + AbsoluteUnits.Points => $"{Value}pt", + _ => throw new ArgumentOutOfRangeException() + }; + } +} diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 3f5b8aa..6035a36 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -9,12 +9,12 @@ public static class ElementLayout private const double DEFAULT_MAX = double.MaxValue; private const double DEFAULT_BORDER_WIDTH = 0f; - internal static UISize Layout(ElementHandle elementHandle, Paper gui) + internal static UISize Layout(ElementHandle elementHandle, Paper gui, in ScalingSettings scalingSettings) { ref var data = ref elementHandle.Data; - var wValue = (UnitValue)data._elementStyle.GetValue(GuiProp.Width); - var hValue = (UnitValue)data._elementStyle.GetValue(GuiProp.Height); + var wValue = (RelativeUnit)data._elementStyle.GetValue(GuiProp.Width); + var hValue = (RelativeUnit)data._elementStyle.GetValue(GuiProp.Height); double width = wValue.IsPixels ? wValue.Value : throw new Exception("Root element must have fixed width"); double height = hValue.IsPixels ? hValue.Value : throw new Exception("Root element must have fixed height"); @@ -23,7 +23,7 @@ internal static UISize Layout(ElementHandle elementHandle, Paper gui) data.LayoutWidth = width; data.LayoutHeight = height; - var size = DoLayout(elementHandle, LayoutType.Column, height, width); + var size = DoLayout(elementHandle, LayoutType.Column, scalingSettings, height, width); // Convert relative positions to absolute positions ComputeAbsolutePositions(ref data, gui); @@ -31,23 +31,23 @@ internal static UISize Layout(ElementHandle elementHandle, Paper gui) return size; } - private static UnitValue GetProp(ref ElementData element, LayoutType parentType, GuiProp row, GuiProp column) - => (UnitValue)element._elementStyle.GetValue(parentType == LayoutType.Row ? row : column); - - private static UnitValue GetMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Width, GuiProp.Height); - private static UnitValue GetCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Height, GuiProp.Width); - private static UnitValue GetMinMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinWidth, GuiProp.MinHeight); - private static UnitValue GetMaxMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxWidth, GuiProp.MaxHeight); - private static UnitValue GetMinCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinHeight, GuiProp.MinWidth); - private static UnitValue GetMaxCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxHeight, GuiProp.MaxWidth); - private static UnitValue GetMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Left, GuiProp.Top); - private static UnitValue GetMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Right, GuiProp.Bottom); - private static UnitValue GetCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Top, GuiProp.Left); - private static UnitValue GetCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Bottom, GuiProp.Right); - private static UnitValue GetChildMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildLeft, GuiProp.ChildTop); - private static UnitValue GetChildMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildRight, GuiProp.ChildBottom); - private static UnitValue GetChildCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildTop, GuiProp.ChildLeft); - private static UnitValue GetChildCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildBottom, GuiProp.ChildRight); + private static RelativeUnit GetProp(ref ElementData element, LayoutType parentType, GuiProp row, GuiProp column) + => (RelativeUnit)element._elementStyle.GetValue(parentType == LayoutType.Row ? row : column); + + private static RelativeUnit GetMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Width, GuiProp.Height); + private static RelativeUnit GetCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Height, GuiProp.Width); + private static RelativeUnit GetMinMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinWidth, GuiProp.MinHeight); + private static RelativeUnit GetMaxMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxWidth, GuiProp.MaxHeight); + private static RelativeUnit GetMinCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinHeight, GuiProp.MinWidth); + private static RelativeUnit GetMaxCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxHeight, GuiProp.MaxWidth); + private static RelativeUnit GetMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Left, GuiProp.Top); + private static RelativeUnit GetMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Right, GuiProp.Bottom); + private static RelativeUnit GetCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Top, GuiProp.Left); + private static RelativeUnit GetCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Bottom, GuiProp.Right); + private static RelativeUnit GetChildMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildLeft, GuiProp.ChildTop); + private static RelativeUnit GetChildMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildRight, GuiProp.ChildBottom); + private static RelativeUnit GetChildCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildTop, GuiProp.ChildLeft); + private static RelativeUnit GetChildCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildBottom, GuiProp.ChildRight); private static IEnumerable GetChildren(ElementHandle elementHandle) { @@ -56,19 +56,19 @@ private static IEnumerable GetChildren(ElementHandle elementHandl yield return new ElementHandle(elementHandle.Owner, childIndex); } } - private static UnitValue GetMainBetween(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.RowBetween, GuiProp.ColBetween); - private static UnitValue GetMinMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinLeft, GuiProp.MinTop); - private static UnitValue GetMaxMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxLeft, GuiProp.MaxTop); - private static UnitValue GetMinMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinRight, GuiProp.MinBottom); - private static UnitValue GetMaxMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxRight, GuiProp.MaxBottom); - private static UnitValue GetMinCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinTop, GuiProp.MinLeft); - private static UnitValue GetMaxCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxTop, GuiProp.MaxLeft); - private static UnitValue GetMinCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinBottom, GuiProp.MinRight); - private static UnitValue GetMaxCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxBottom, GuiProp.MaxRight); - private static UnitValue GetBorderMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderLeft, GuiProp.BorderTop); - private static UnitValue GetBorderMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderRight, GuiProp.BorderBottom); - private static UnitValue GetBorderCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderTop, GuiProp.BorderLeft); - private static UnitValue GetBorderCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderBottom, GuiProp.BorderRight); + private static RelativeUnit GetMainBetween(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.RowBetween, GuiProp.ColBetween); + private static RelativeUnit GetMinMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinLeft, GuiProp.MinTop); + private static RelativeUnit GetMaxMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxLeft, GuiProp.MaxTop); + private static RelativeUnit GetMinMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinRight, GuiProp.MinBottom); + private static RelativeUnit GetMaxMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxRight, GuiProp.MaxBottom); + private static RelativeUnit GetMinCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinTop, GuiProp.MinLeft); + private static RelativeUnit GetMaxCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxTop, GuiProp.MaxLeft); + private static RelativeUnit GetMinCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinBottom, GuiProp.MinRight); + private static RelativeUnit GetMaxCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxBottom, GuiProp.MaxRight); + private static RelativeUnit GetBorderMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderLeft, GuiProp.BorderTop); + private static RelativeUnit GetBorderMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderRight, GuiProp.BorderBottom); + private static RelativeUnit GetBorderCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderTop, GuiProp.BorderLeft); + private static RelativeUnit GetBorderCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderBottom, GuiProp.BorderRight); private static (double, double)? ContentSizing(ElementHandle elementHandle, LayoutType parentLayoutType, double? parentMain, double? parentCross) { @@ -119,44 +119,44 @@ private static (double, double)? GetContentSize(ElementHandle elementHandle, Lay return contentSize; } - private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLayoutType, double parentMain, double parentCross) + private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLayoutType, in ScalingSettings scalingSettings, double parentMain, double parentCross) { ref var element = ref elementHandle.Data; LayoutType layoutType = element.LayoutType; - UnitValue main = GetMain(ref element, parentLayoutType); - UnitValue cross = GetCross(ref element, parentLayoutType); + RelativeUnit main = GetMain(ref element, parentLayoutType); + RelativeUnit cross = GetCross(ref element, parentLayoutType); double minMain = main.IsStretch ? DEFAULT_MIN - : GetMinMain(ref element, parentLayoutType).ToPx(parentMain, DEFAULT_MIN); + : GetMinMain(ref element, parentLayoutType).ToPx(parentMain, DEFAULT_MIN, scalingSettings); double maxMain = main.IsStretch ? DEFAULT_MAX - : GetMaxMain(ref element, parentLayoutType).ToPx(parentMain, DEFAULT_MAX); + : GetMaxMain(ref element, parentLayoutType).ToPx(parentMain, DEFAULT_MAX, scalingSettings); double minCross = cross.IsStretch ? DEFAULT_MIN - : GetMinCross(ref element, parentLayoutType).ToPx(parentCross, DEFAULT_MIN); + : GetMinCross(ref element, parentLayoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); double maxCross = cross.IsStretch ? DEFAULT_MAX - : GetMaxCross(ref element, parentLayoutType).ToPx(parentCross, DEFAULT_MAX); + : GetMaxCross(ref element, parentLayoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); // Compute main-axis size double computedMain = 0; if (main.IsStretch) computedMain = parentMain; - else if (main.IsPixels || main.IsPercentage) - computedMain = main.ToPx(parentMain, 100f); + else if (main.IsPixels || main.IsPoints || main.IsPercentage) + computedMain = main.ToPx(parentMain, 100f, scalingSettings); // Auto stays at 0 // Compute cross-axis size double computedCross = 0; if(cross.IsStretch) computedCross = parentCross; - else if (cross.IsPixels || cross.IsPercentage) - computedCross = cross.ToPx(parentCross, 100f); + else if (cross.IsPixels || cross.IsPoints || cross.IsPercentage) + computedCross = cross.ToPx(parentCross, 100f, scalingSettings); // Auto stays at 0 @@ -207,18 +207,18 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay } var borderMainBeforeUnit = GetBorderMainBefore(ref element, parentLayoutType); - double borderMainBefore = borderMainBeforeUnit.ToPx(computedMain, DEFAULT_BORDER_WIDTH); + double borderMainBefore = borderMainBeforeUnit.ToPx(computedMain, DEFAULT_BORDER_WIDTH, scalingSettings); var borderMainAfterUnit = GetBorderMainAfter(ref element, parentLayoutType); - double borderMainAfter = borderMainAfterUnit.ToPx(computedMain, DEFAULT_BORDER_WIDTH); + double borderMainAfter = borderMainAfterUnit.ToPx(computedMain, DEFAULT_BORDER_WIDTH, scalingSettings); var borderCrossBeforeUnit = GetBorderCrossBefore(ref element, parentLayoutType); - double borderCrossBefore = borderCrossBeforeUnit.ToPx(computedCross, DEFAULT_BORDER_WIDTH); + double borderCrossBefore = borderCrossBeforeUnit.ToPx(computedCross, DEFAULT_BORDER_WIDTH, scalingSettings); var borderCrossAfterUnit = GetBorderCrossAfter(ref element, parentLayoutType); - double borderCrossAfter = borderCrossAfterUnit.ToPx(computedCross, DEFAULT_BORDER_WIDTH); + double borderCrossAfter = borderCrossAfterUnit.ToPx(computedCross, DEFAULT_BORDER_WIDTH, scalingSettings); // Pre-allocate and filter in single pass to avoid LINQ overhead var visibleChildren = new List(); var parentDirectedChildren = new List(); - + foreach (int childIdx in element.ChildIndices) { var childData = elementHandle.Owner.GetElementData(childIdx); @@ -229,7 +229,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay parentDirectedChildren.Add(childIdx); } } - + int numChildren = visibleChildren.Count; int numParentDirectedChildren = parentDirectedChildren.Count; @@ -284,11 +284,11 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay var mainAxis = new List(); // Parent overrides for child auto space - UnitValue elementChildMainBefore = GetChildMainBefore(ref element, layoutType); - UnitValue elementChildMainAfter = GetChildMainAfter(ref element, layoutType); - UnitValue elementChildCrossBefore = GetChildCrossBefore(ref element, layoutType); - UnitValue elementChildCrossAfter = GetChildCrossAfter(ref element, layoutType); - UnitValue elementChildMainBetween = GetMainBetween(ref element, layoutType); + RelativeUnit elementChildMainBefore = GetChildMainBefore(ref element, layoutType); + RelativeUnit elementChildMainAfter = GetChildMainAfter(ref element, layoutType); + RelativeUnit elementChildCrossBefore = GetChildCrossBefore(ref element, layoutType); + RelativeUnit elementChildCrossAfter = GetChildCrossAfter(ref element, layoutType); + RelativeUnit elementChildMainBetween = GetMainBetween(ref element, layoutType); // Get first and last parent-directed children for spacing int? first = parentDirectedChildren.Count > 0 ? 0 : null; @@ -302,25 +302,25 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay var childHandle = new ElementHandle(elementHandle.Owner, childIndex); // Get desired space and size - UnitValue childMainBefore = GetMainBefore(ref child, layoutType); - UnitValue childMain = GetMain(ref child, layoutType); - UnitValue childMainAfter = GetMainAfter(ref child, layoutType); + RelativeUnit childMainBefore = GetMainBefore(ref child, layoutType); + RelativeUnit childMain = GetMain(ref child, layoutType); + RelativeUnit childMainAfter = GetMainAfter(ref child, layoutType); - UnitValue childCrossBefore = GetCrossBefore(ref child, layoutType); - UnitValue childCross = GetCross(ref child, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child, layoutType); + RelativeUnit childCrossBefore = GetCrossBefore(ref child, layoutType); + RelativeUnit childCross = GetCross(ref child, layoutType); + RelativeUnit childCrossAfter = GetCrossAfter(ref child, layoutType); // Get constraints - UnitValue childMinCrossBefore = GetMinCrossBefore(ref child, layoutType); - UnitValue childMaxCrossBefore = GetMaxCrossBefore(ref child, layoutType); - UnitValue childMinCrossAfter = GetMinCrossAfter(ref child, layoutType); - UnitValue childMaxCrossAfter = GetMaxCrossAfter(ref child, layoutType); - UnitValue childMinMainBefore = GetMinMainBefore(ref child, layoutType); - UnitValue childMaxMainBefore = GetMaxMainBefore(ref child, layoutType); - UnitValue childMinMainAfter = GetMinMainAfter(ref child, layoutType); - UnitValue childMaxMainAfter = GetMaxMainAfter(ref child, layoutType); - UnitValue childMinMain = GetMinMain(ref child, layoutType); - UnitValue childMaxMain = GetMaxMain(ref child, layoutType); + RelativeUnit childMinCrossBefore = GetMinCrossBefore(ref child, layoutType); + RelativeUnit childMaxCrossBefore = GetMaxCrossBefore(ref child, layoutType); + RelativeUnit childMinCrossAfter = GetMinCrossAfter(ref child, layoutType); + RelativeUnit childMaxCrossAfter = GetMaxCrossAfter(ref child, layoutType); + RelativeUnit childMinMainBefore = GetMinMainBefore(ref child, layoutType); + RelativeUnit childMaxMainBefore = GetMaxMainBefore(ref child, layoutType); + RelativeUnit childMinMainAfter = GetMinMainAfter(ref child, layoutType); + RelativeUnit childMaxMainAfter = GetMaxMainAfter(ref child, layoutType); + RelativeUnit childMinMain = GetMinMain(ref child, layoutType); + RelativeUnit childMaxMain = GetMaxMain(ref child, layoutType); // Apply parent overrides to auto spacing if (childMainBefore.IsAuto && first == i) @@ -364,8 +364,8 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay i, // Use list index for StretchItem childMainBefore.Value, StretchItem.ItemTypes.Before, - childMinMainBefore.ToPx(actualParentMain, DEFAULT_MIN), - childMaxMainBefore.ToPx(actualParentMain, DEFAULT_MAX) + childMinMainBefore.ToPx(actualParentMain, DEFAULT_MIN, scalingSettings), + childMaxMainBefore.ToPx(actualParentMain, DEFAULT_MAX, scalingSettings) )); } @@ -376,8 +376,8 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay i, // Use list index for StretchItem childMain.Value, StretchItem.ItemTypes.Size, - childMinMain.ToPx(actualParentMain, DEFAULT_MIN), - childMaxMain.ToPx(actualParentMain, DEFAULT_MAX) + childMinMain.ToPx(actualParentMain, DEFAULT_MIN, scalingSettings), + childMaxMain.ToPx(actualParentMain, DEFAULT_MAX, scalingSettings) )); } @@ -388,23 +388,23 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay i, // Use list index for StretchItem childMainAfter.Value, StretchItem.ItemTypes.After, - childMinMainAfter.ToPx(actualParentMain, DEFAULT_MIN), - childMaxMainAfter.ToPx(actualParentMain, DEFAULT_MAX) + childMinMainAfter.ToPx(actualParentMain, DEFAULT_MIN, scalingSettings), + childMaxMainAfter.ToPx(actualParentMain, DEFAULT_MAX, scalingSettings) )); } // Compute fixed-size child spaces double computedChildCrossBefore = childCrossBefore.ToPxClamped( - actualParentCross, 0f, childMinCrossBefore, childMaxCrossBefore); + actualParentCross, 0f, childMinCrossBefore, childMaxCrossBefore, scalingSettings); double computedChildCrossAfter = childCrossAfter.ToPxClamped( - actualParentCross, 0f, childMinCrossAfter, childMaxCrossAfter); + actualParentCross, 0f, childMinCrossAfter, childMaxCrossAfter, scalingSettings); double computedChildMainBefore = childMainBefore.ToPxClamped( - actualParentMain, 0f, childMinMainBefore, childMaxMainBefore); + actualParentMain, 0f, childMinMainBefore, childMaxMainBefore, scalingSettings); double computedChildMainAfter = childMainAfter.ToPxClamped( - actualParentMain, 0f, childMinMainAfter, childMaxMainAfter); + actualParentMain, 0f, childMinMainAfter, childMaxMainAfter, scalingSettings); double computedChildMain = 0f; - double computedChildCross = childCross.ToPx(actualParentCross, 0f); + double computedChildCross = childCross.ToPx(actualParentCross, 0f, scalingSettings); // Get auto min cross size if needed if (GetMinCross(ref child, layoutType).IsAuto) @@ -420,7 +420,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay // Compute fixed-size child main and cross for non-stretch children if (!childMain.IsStretch && !childCross.IsStretch) { - var childSize = DoLayout(childHandle, layoutType, actualParentMain, actualParentCross); + var childSize = DoLayout(childHandle, layoutType, scalingSettings, actualParentMain, actualParentCross); computedChildMain = childSize.Main; computedChildCross = childSize.Cross; } @@ -481,9 +481,9 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay GetCross(ref child.Element.Data, layoutType).IsAuto) continue; - UnitValue childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - UnitValue childCross = GetCross(ref child.Element.Data, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); + RelativeUnit childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeUnit childCross = GetCross(ref child.Element.Data, layoutType); + RelativeUnit childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); if (childCrossBefore.IsAuto) childCrossBefore = elementChildCrossBefore; @@ -498,9 +498,9 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay if (childCrossBefore.IsStretch) { double childMinCrossBefore = GetMinCrossBefore(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MIN); + .ToPx(actualParentCross, DEFAULT_MIN, scalingSettings); double childMaxCrossBefore = GetMaxCrossBefore(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MAX); + .ToPx(actualParentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossBefore.Value; child.CrossBefore = 0f; @@ -517,9 +517,9 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay if (childCross.IsStretch) { double childMinCross = GetMinCross(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MIN); + .ToPx(actualParentCross, DEFAULT_MIN, scalingSettings); double childMaxCross = GetMaxCross(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MAX); + .ToPx(actualParentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCross.Value; child.Cross = 0f; @@ -536,9 +536,9 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay if (childCrossAfter.IsStretch) { double childMinCrossAfter = GetMinCrossAfter(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MIN); + .ToPx(actualParentCross, DEFAULT_MIN, scalingSettings); double childMaxCrossAfter = GetMaxCrossAfter(ref child.Element.Data, layoutType) - .ToPx(actualParentCross, DEFAULT_MAX); + .ToPx(actualParentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossAfter.Value; child.CrossAfter = 0f; @@ -575,6 +575,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay var childSize = DoLayout( child.Element, layoutType, + scalingSettings, actualParentMain, actualCross, actualCross); @@ -680,7 +681,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay double childCross = GetCross(ref child.Element.Data, layoutType).IsStretch ? child.Cross : actualParentCross; - var childSize = DoLayout(child.Element, layoutType, actualMain, childCross); + var childSize = DoLayout(child.Element, layoutType, scalingSettings, actualMain, childCross); child.Cross = childSize.Cross; crossMax = Math.Max(crossMax, child.CrossBefore + child.Cross + child.CrossAfter); @@ -771,12 +772,12 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay foreach (var childHandle in GetChildren(elementHandle)) { if (!childHandle.Data.Visible || childHandle.Data.PositionType != PositionType.SelfDirected) continue; - UnitValue childMainBefore = GetMainBefore(ref childHandle.Data, layoutType); - UnitValue childMain = GetMain(ref childHandle.Data, layoutType); - UnitValue childMainAfter = GetMainAfter(ref childHandle.Data, layoutType); - UnitValue childCrossBefore = GetCrossBefore(ref childHandle.Data, layoutType); - UnitValue childCross = GetCross(ref childHandle.Data, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref childHandle.Data, layoutType); + RelativeUnit childMainBefore = GetMainBefore(ref childHandle.Data, layoutType); + RelativeUnit childMain = GetMain(ref childHandle.Data, layoutType); + RelativeUnit childMainAfter = GetMainAfter(ref childHandle.Data, layoutType); + RelativeUnit childCrossBefore = GetCrossBefore(ref childHandle.Data, layoutType); + RelativeUnit childCross = GetCross(ref childHandle.Data, layoutType); + RelativeUnit childCrossAfter = GetCrossAfter(ref childHandle.Data, layoutType); // Apply parent overrides if (childMainBefore.IsAuto) @@ -790,13 +791,13 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay // Compute fixed spaces double computedChildCrossBefore = childCrossBefore.ToPxClamped( - actualParentCross, 0f, GetMinCrossBefore(ref childHandle.Data, layoutType), GetMaxCrossBefore(ref childHandle.Data, layoutType)); + actualParentCross, 0f, GetMinCrossBefore(ref childHandle.Data, layoutType), GetMaxCrossBefore(ref childHandle.Data, layoutType), scalingSettings); double computedChildCrossAfter = childCrossAfter.ToPxClamped( - actualParentCross, 0f, GetMinCrossAfter(ref childHandle.Data, layoutType), GetMaxCrossAfter(ref childHandle.Data, layoutType)); + actualParentCross, 0f, GetMinCrossAfter(ref childHandle.Data, layoutType), GetMaxCrossAfter(ref childHandle.Data, layoutType), scalingSettings); double computedChildMainBefore = childMainBefore.ToPxClamped( - actualParentMain, 0f, GetMinMainBefore(ref childHandle.Data, layoutType), GetMaxMainBefore(ref childHandle.Data, layoutType)); + actualParentMain, 0f, GetMinMainBefore(ref childHandle.Data, layoutType), GetMaxMainBefore(ref childHandle.Data, layoutType), scalingSettings); double computedChildMainAfter = childMainAfter.ToPxClamped( - actualParentMain, 0f, GetMinMainAfter(ref childHandle.Data, layoutType), GetMaxMainAfter(ref childHandle.Data, layoutType)); + actualParentMain, 0f, GetMinMainAfter(ref childHandle.Data, layoutType), GetMaxMainAfter(ref childHandle.Data, layoutType), scalingSettings); double computedChildMain = 0f; double computedChildCross = 0f; @@ -804,7 +805,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay // Compute fixed-size child sizes if (!childMain.IsStretch && !childCross.IsStretch) { - var childSize = DoLayout(childHandle, layoutType, actualParentMain, actualParentCross); + var childSize = DoLayout(childHandle, layoutType, scalingSettings, actualParentMain, actualParentCross); computedChildMain = childSize.Main; computedChildCross = childSize.Cross; } @@ -823,7 +824,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay for (int i = parentDirectedChildren.Count; i < children.Count; i++) { var child = children[i]; - ProcessChildCrossStretching(child, layoutType, actualParentCross, actualParentMain, + ProcessChildCrossStretching(child, layoutType, scalingSettings, actualParentCross, actualParentMain, borderCrossBefore, borderCrossAfter, elementChildCrossBefore, elementChildCrossAfter, i); } @@ -831,14 +832,14 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay for (int i = parentDirectedChildren.Count; i < children.Count; i++) { var child = children[i]; - ProcessChildMainStretching(child, layoutType, actualParentMain, actualParentCross, + ProcessChildMainStretching(child, layoutType, scalingSettings, actualParentMain, actualParentCross, borderMainBefore, borderMainAfter, elementChildMainBefore, elementChildMainAfter, i); } // Compute stretch cross spacing for auto-sized children foreach (var child in children) { - ProcessChildCrossSpacing(child, layoutType, actualParentCross, + ProcessChildCrossSpacing(child, layoutType, scalingSettings, actualParentCross, borderCrossBefore, borderCrossAfter, elementChildCrossBefore, elementChildCrossAfter); } @@ -913,17 +914,18 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay private static void ProcessChildCrossStretching( ChildElementInfo child, LayoutType layoutType, + in ScalingSettings scalingSettings, double parentCross, double parentMain, double borderCrossBefore, double borderCrossAfter, - UnitValue elementChildCrossBefore, - UnitValue elementChildCrossAfter, + RelativeUnit elementChildCrossBefore, + RelativeUnit elementChildCrossAfter, int childIndex) { - UnitValue childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - UnitValue childCross = GetCross(ref child.Element.Data, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); + RelativeUnit childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeUnit childCross = GetCross(ref child.Element.Data, layoutType); + RelativeUnit childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childCrossBefore.IsAuto) @@ -937,8 +939,8 @@ private static void ProcessChildCrossStretching( // Collect stretch cross items if (childCrossBefore.IsStretch) { - double min = GetMinCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN); - double max = GetMaxCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX); + double min = GetMinCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); + double max = GetMaxCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossBefore.Value; child.CrossBefore = 0f; @@ -948,8 +950,8 @@ private static void ProcessChildCrossStretching( if (childCross.IsStretch) { - double min = GetMinCross(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN); - double max = GetMaxCross(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX); + double min = GetMinCross(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); + double max = GetMaxCross(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCross.Value; child.Cross = 0f; @@ -959,8 +961,8 @@ private static void ProcessChildCrossStretching( if (childCrossAfter.IsStretch) { - double min = GetMinCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN); - double max = GetMaxCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX); + double min = GetMinCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); + double max = GetMaxCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossAfter.Value; child.CrossAfter = 0f; @@ -983,7 +985,7 @@ private static void ProcessChildCrossStretching( if (item.ItemType == StretchItem.ItemTypes.Size && !GetMain(ref child.Element.Data, layoutType).IsStretch) { - var childSize = DoLayout(child.Element, layoutType, parentMain, actualCross); + var childSize = DoLayout(child.Element, layoutType, scalingSettings, parentMain, actualCross); if (GetMinCross(ref child.Element.Data, layoutType).IsAuto) { item.Min = childSize.Cross; @@ -1033,17 +1035,18 @@ private static void ProcessChildCrossStretching( private static void ProcessChildMainStretching( ChildElementInfo child, LayoutType layoutType, + in ScalingSettings scalingSettings, double parentMain, double parentCross, double borderMainBefore, double borderMainAfter, - UnitValue elementChildMainBefore, - UnitValue elementChildMainAfter, + RelativeUnit elementChildMainBefore, + RelativeUnit elementChildMainAfter, int childIndex) { - UnitValue childMainBefore = GetMainBefore(ref child.Element.Data, layoutType); - UnitValue childMain = GetMain(ref child.Element.Data, layoutType); - UnitValue childMainAfter = GetMainAfter(ref child.Element.Data, layoutType); + RelativeUnit childMainBefore = GetMainBefore(ref child.Element.Data, layoutType); + RelativeUnit childMain = GetMain(ref child.Element.Data, layoutType); + RelativeUnit childMainAfter = GetMainAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childMainBefore.IsAuto) @@ -1057,8 +1060,8 @@ private static void ProcessChildMainStretching( // Collect stretch main items if (childMainBefore.IsStretch) { - double min = GetMinMainBefore(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN); - double max = GetMaxMainBefore(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX); + double min = GetMinMainBefore(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN, scalingSettings); + double max = GetMaxMainBefore(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX, scalingSettings); mainFlexSum += childMainBefore.Value; mainAxis.Add(new StretchItem(childIndex, childMainBefore.Value, StretchItem.ItemTypes.Before, min, max)); @@ -1066,8 +1069,8 @@ private static void ProcessChildMainStretching( if (childMain.IsStretch) { - double min = GetMinMain(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN); - double max = GetMaxMain(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX); + double min = GetMinMain(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN, scalingSettings); + double max = GetMaxMain(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX, scalingSettings); mainFlexSum += childMain.Value; mainAxis.Add(new StretchItem(childIndex, childMain.Value, StretchItem.ItemTypes.Size, min, max)); @@ -1075,8 +1078,8 @@ private static void ProcessChildMainStretching( if (childMainAfter.IsStretch) { - double min = GetMinMainAfter(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN); - double max = GetMaxMainAfter(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX); + double min = GetMinMainAfter(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MIN, scalingSettings); + double max = GetMaxMainAfter(ref child.Element.Data, layoutType).ToPx(parentMain, DEFAULT_MAX, scalingSettings); mainFlexSum += childMainAfter.Value; mainAxis.Add(new StretchItem(childIndex, childMainAfter.Value, StretchItem.ItemTypes.After, min, max)); @@ -1100,7 +1103,7 @@ private static void ProcessChildMainStretching( double childCross = GetCross(ref child.Element.Data, layoutType).IsStretch ? child.Cross : parentCross; - var childSize = DoLayout(child.Element, layoutType, actualMain, childCross); + var childSize = DoLayout(child.Element, layoutType, scalingSettings, actualMain, childCross); child.Cross = childSize.Cross; if (GetMinMain(ref child.Element.Data, layoutType).IsAuto) @@ -1151,14 +1154,15 @@ private static void ProcessChildMainStretching( private static void ProcessChildCrossSpacing( ChildElementInfo child, LayoutType layoutType, + in ScalingSettings scalingSettings, double parentCross, double borderCrossBefore, double borderCrossAfter, - UnitValue elementChildCrossBefore, - UnitValue elementChildCrossAfter) + RelativeUnit elementChildCrossBefore, + RelativeUnit elementChildCrossAfter) { - UnitValue childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); + RelativeUnit childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeUnit childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childCrossBefore.IsAuto) @@ -1177,8 +1181,8 @@ private static void ProcessChildCrossSpacing( // Collect stretch cross items if (childCrossBefore.IsStretch) { - double min = GetMinCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN); - double max = GetMaxCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX); + double min = GetMinCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); + double max = GetMaxCrossBefore(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossBefore.Value; child.CrossBefore = 0f; @@ -1188,8 +1192,8 @@ private static void ProcessChildCrossSpacing( if (childCrossAfter.IsStretch) { - double min = GetMinCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN); - double max = GetMaxCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX); + double min = GetMinCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MIN, scalingSettings); + double max = GetMaxCrossAfter(ref child.Element.Data, layoutType).ToPx(parentCross, DEFAULT_MAX, scalingSettings); crossFlexSum += childCrossAfter.Value; child.CrossAfter = 0f; @@ -1246,9 +1250,9 @@ private static void ProcessChildCrossSpacing( } } - private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLayoutType, double parentMain, double parentCross, double? fixedCross) + private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLayoutType, in ScalingSettings scalingSettings, double parentMain, double parentCross, double? fixedCross) { - var size = DoLayout(elementHandle, parentLayoutType, parentMain, parentCross); + var size = DoLayout(elementHandle, parentLayoutType, scalingSettings, parentMain, parentCross); // If we're calculating a particular cross size constraint, update the element with the fixed cross size if (fixedCross.HasValue) @@ -1301,11 +1305,11 @@ internal static Vector2 ProcessText(this ref ElementData element, Paper gui, flo { var settings = TextLayoutSettings.Default; - settings.WordSpacing = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.WordSpacing)); - settings.LetterSpacing = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.LetterSpacing)); + settings.WordSpacing = (float)((AbsoluteUnit)element._elementStyle.GetValue(GuiProp.WordSpacing)).ToPx(gui.ScalingSettings); + settings.LetterSpacing = (float)((AbsoluteUnit)element._elementStyle.GetValue(GuiProp.LetterSpacing)).ToPx(gui.ScalingSettings); settings.LineHeight = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.LineHeight)); settings.TabSize = (int)element._elementStyle.GetValue(GuiProp.TabSize); - settings.PixelSize = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.FontSize)); + settings.PixelSize = (float)((AbsoluteUnit)element._elementStyle.GetValue(GuiProp.FontSize)).ToPx(gui.ScalingSettings); if(element.TextAlignment == TextAlignment.Left || element.TextAlignment == TextAlignment.MiddleLeft || element.TextAlignment == TextAlignment.BottomLeft) settings.Alignment = Scribe.TextAlignment.Left; diff --git a/Paper/LayoutEngine/RelativeUnit.cs b/Paper/LayoutEngine/RelativeUnit.cs new file mode 100644 index 0000000..dcb9c58 --- /dev/null +++ b/Paper/LayoutEngine/RelativeUnit.cs @@ -0,0 +1,253 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +using Prowl.PaperUI; +using Prowl.Vector; + +namespace Prowl.PaperUI.LayoutEngine +{ + /// + /// Represents a layout relative value with a unit type for UI layout measurements. + /// Supports pixels, points, percentages, auto-sizing, and stretch units with interpolation capabilities. + /// + public struct RelativeUnit : IEquatable + { + /// + /// Helper class for interpolation between two RelativeUnit instances. + /// Using a simplified class approach to avoid struct cycles. + /// + private class LerpData + { + public readonly RelativeUnit Start; + public readonly RelativeUnit End; + public readonly double Progress; + + public LerpData(RelativeUnit start, RelativeUnit end, double progress) + { + Start = start; + End = end; + Progress = progress; + } + } + + /// The unit type of this value + public RelativeUnits Type { get; set; } = RelativeUnits.Auto; + + /// The numeric value in the specified units + public double Value { get; set; } = 0f; + + /// Additional pixel offset when using percentage units + public double PercentPixelOffset { get; set; } = default; + + /// Data for interpolation between two RelativeUnits (null when not interpolating) + private LerpData? _lerpData = null; + + /// + /// Creates a default RelativeUnit with Auto units. + /// + public RelativeUnit() { } + + /// + /// Creates a RelativeUnit with the specified type and value. + /// + /// The unit type + /// The numeric value + /// Additional pixel offset for percentage units + public RelativeUnit(RelativeUnits type, double value = 0f, double offset = 0f) + { + Type = type; + Value = value; + PercentPixelOffset = offset; + } + + #region Type Checking Properties + + /// Returns true if this value is using Pixel units + public bool IsPixels => Type == RelativeUnits.Pixels; + + /// Returns true if this value is using Point units + public bool IsPoints => Type == RelativeUnits.Points; + + /// Returns true if this value is using Auto units + public bool IsAuto => Type == RelativeUnits.Auto; + + /// Returns true if this value is using Stretch units + public bool IsStretch => Type == RelativeUnits.Stretch; + + /// Returns true if this value is using Percentage units + public bool IsPercentage => Type == RelativeUnits.Percentage; + + #endregion + + /// + /// Converts this unit value to pixels based on the parent's size. + /// + /// The parent element's size in pixels + /// Default value to use for Auto and Stretch units + /// Settings to use for scaling calculations + /// Size in pixels + public readonly double ToPx(double parentValue, double defaultValue, in ScalingSettings scalingSettings) + { + // Handle interpolation if active + if (_lerpData != null) + { + var startPx = _lerpData.Start.ToPx(parentValue, defaultValue, scalingSettings); + var endPx = _lerpData.End.ToPx(parentValue, defaultValue, scalingSettings); + return startPx + (endPx - startPx) * _lerpData.Progress; + } + + // Convert based on unit type + return Type switch { + RelativeUnits.Pixels => Value, + RelativeUnits.Points => Value * scalingSettings.ContentScale, + RelativeUnits.Percentage => ((Value / 100f) * parentValue) + PercentPixelOffset, + _ => defaultValue + }; + } + + /// + /// Converts this unit value to pixels and clamps it between minimum and maximum values. + /// + /// The parent element's size in pixels + /// Default value to use for Auto and Stretch units + /// Minimum allowed value + /// Maximum allowed value + /// Settings to use for scaling calculations + /// Size in pixels, clamped between min and max + public readonly double ToPxClamped(double parentValue, double defaultValue, in RelativeUnit min, in RelativeUnit max, in ScalingSettings scalingSettings) + { + double minValue = min.ToPx(parentValue, double.MinValue, scalingSettings); + double maxValue = max.ToPx(parentValue, double.MaxValue, scalingSettings); + double value = ToPx(parentValue, defaultValue, scalingSettings); + + return Math.Min(maxValue, Math.Max(minValue, value)); + } + + /// + /// Linearly interpolates between two RelativeUnit instances. + /// In reality, it creates a new RelativeUnit with special interpolation data which is calculated when ToPx is called. + /// + /// Starting value + /// Ending value + /// Interpolation factor (0.0 to 1.0) + /// Interpolated RelativeUnit + public static RelativeUnit Lerp(in RelativeUnit a, in RelativeUnit b, double blendFactor) + { + // Ensure blend factor is between 0 and 1 + blendFactor = Math.Clamp(blendFactor, 0f, 1f); + + // If units are the same, we can blend directly + if (a.Type == b.Type) + { + return new RelativeUnit( + a.Type, + a.Value + (b.Value - a.Value) * blendFactor, + a.PercentPixelOffset + (b.PercentPixelOffset - a.PercentPixelOffset) * blendFactor + ); + } + + // If units are different, use interpolation data + var result = new RelativeUnit { + Type = a.Type, + Value = a.Value, + PercentPixelOffset = a.PercentPixelOffset, + _lerpData = new LerpData(a, b, blendFactor) + }; + return result; + } + + /// + /// Creates a deep copy of this RelativeUnit. + /// + /// A new RelativeUnit with the same properties + public readonly RelativeUnit Clone() => new RelativeUnit { + Type = Type, + Value = Value, + PercentPixelOffset = PercentPixelOffset, + _lerpData = _lerpData != null ? new LerpData(_lerpData.Start, _lerpData.End, _lerpData.Progress) : null + }; + + #region Implicit Conversions + + /// + /// Implicitly converts an integer to a point unit RelativeUnit. + /// + public static implicit operator RelativeUnit(int value) + { + return new RelativeUnit(RelativeUnits.Points, value); + } + + /// + /// Implicitly converts a double to a point unit RelativeUnit. + /// + public static implicit operator RelativeUnit(double value) + { + return new RelativeUnit(RelativeUnits.Points, value); + } + + #endregion + + #region Equality and Hashing + + public static bool operator ==(RelativeUnit left, RelativeUnit right) + { + return left.Equals(right); + } + + public static bool operator !=(RelativeUnit left, RelativeUnit right) + { + return !left.Equals(right); + } + + /// + /// Compares this RelativeUnit with another object for equality. + /// + public override readonly bool Equals(object? obj) + { + return obj is RelativeUnit other && Equals(other); + } + + public readonly bool Equals(RelativeUnit other) + { + // First, check the basic properties + bool basicPropertiesEqual = Type == other.Type && + Value.Equals(other.Value) && + PercentPixelOffset.Equals(other.PercentPixelOffset); + + // If either value isn't interpolating, they're equal only if both aren't + if (_lerpData is null || other._lerpData is null) + return basicPropertiesEqual && _lerpData is null && other._lerpData is null; + + // Both values are interpolating – compare their interpolation data safely + var thisLerp = _lerpData; + var otherLerp = other._lerpData; + bool lerpPropsEqual = thisLerp.Start.Equals(otherLerp.Start) && + thisLerp.End.Equals(otherLerp.End) && + thisLerp.Progress.Equals(otherLerp.Progress); + + return basicPropertiesEqual && lerpPropsEqual; + } + + /// + /// Returns a hash code for this RelativeUnit. + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(_lerpData, (int)Type, Value, PercentPixelOffset); + } + + #endregion + + /// + /// Returns a string representation of this RelativeUnit. + /// + public override readonly string ToString() => Type switch { + RelativeUnits.Pixels => $"{Value}px", + RelativeUnits.Points => $"{Value}pt", + RelativeUnits.Percentage => $"{Value}% + {PercentPixelOffset}", + RelativeUnits.Stretch => $"Stretch({Value})", + RelativeUnits.Auto => "Auto", + _ => throw new ArgumentOutOfRangeException() + }; + } +} diff --git a/Paper/LayoutEngine/ScalingSettings.cs b/Paper/LayoutEngine/ScalingSettings.cs new file mode 100644 index 0000000..d4cca8c --- /dev/null +++ b/Paper/LayoutEngine/ScalingSettings.cs @@ -0,0 +1,27 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +namespace Prowl.PaperUI.LayoutEngine +{ + /// + /// Defines how the UI is scaled. + /// + /// + /// This is a struct mainly because there are so many other double-type parameters. + /// + public struct ScalingSettings + { + /// + /// The scaling factor applied to point units. + /// Eg: A value of 2 means that each point is equal to 2 pixels. + /// + public double ContentScale = 1; + + public ScalingSettings() { } + + public ScalingSettings(double contentScale) + { + ContentScale = contentScale; + } + } +} diff --git a/Paper/LayoutEngine/UnitValue.cs b/Paper/LayoutEngine/UnitValue.cs index 66f2f32..47ff48c 100644 --- a/Paper/LayoutEngine/UnitValue.cs +++ b/Paper/LayoutEngine/UnitValue.cs @@ -1,257 +1,40 @@ -using Prowl.PaperUI; +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. namespace Prowl.PaperUI.LayoutEngine { /// - /// Represents a value with a unit type for UI layout measurements. - /// Supports pixels, percentages, auto-sizing, and stretch units with interpolation capabilities. + /// Contains constants and helper methods for creating AbsoluteUnits and RelativeUnits. /// - public struct UnitValue + public static class UnitValue { - /// - /// Helper class for interpolation between two UnitValue instances. - /// Using a simplified class approach to avoid struct cycles. - /// - private class LerpData - { - public readonly UnitValue Start; - public readonly UnitValue End; - public readonly double Progress; - - public LerpData(UnitValue start, UnitValue end, double progress) - { - Start = start; - End = end; - Progress = progress; - } - } - - /// The unit type of this value - public Units Type { get; set; } = Units.Auto; - - /// The numeric value in the specified units - public double Value { get; set; } = 0f; - - /// Additional pixel offset when using percentage units - public double PercentPixelOffset { get; set; } = 0f; - - /// Data for interpolation between two UnitValues (null when not interpolating) - private LerpData? _lerpData = null; + public static readonly AbsoluteUnit ZeroPixels = new AbsoluteUnit(AbsoluteUnits.Pixels, 0); + public static readonly RelativeUnit Auto = new RelativeUnit(RelativeUnits.Auto); + public static readonly RelativeUnit StretchOne = new RelativeUnit(RelativeUnits.Stretch, 1); /// - /// Creates a default UnitValue with Auto units. + /// Creates a Pixel unit value. /// - public UnitValue() { } + /// Size in pixels + public static AbsoluteUnit Pixels(double value) => new AbsoluteUnit(AbsoluteUnits.Pixels, value); /// - /// Creates a UnitValue with the specified type and value. + /// Creates a Points unit value. /// - /// The unit type - /// The numeric value - /// Additional pixel offset for percentage units - public UnitValue(Units type, double value = 0f, double offset = 0f) - { - Type = type; - Value = value; - PercentPixelOffset = offset; - } - - #region Factory Methods - - /// Pre-allocated common values to avoid allocations - public static readonly UnitValue Auto = new UnitValue(Units.Auto); - public static readonly UnitValue ZeroPixels = new UnitValue(Units.Pixels, 0); - public static readonly UnitValue StretchOne = new UnitValue(Units.Stretch, 1); + /// Size in points + public static AbsoluteUnit Points(double value) => new AbsoluteUnit(AbsoluteUnits.Points, value); /// /// Creates a Stretch unit value with the specified factor. /// /// Stretch factor (relative to other stretch elements) - public static UnitValue Stretch(double factor = 1f) => new UnitValue(Units.Stretch, factor); - - /// - /// Creates a Pixel unit value. - /// - /// Size in pixels - public static UnitValue Pixels(double value) => new UnitValue(Units.Pixels, value); + public static RelativeUnit Stretch(double factor = 1f) => new RelativeUnit(RelativeUnits.Stretch, factor); /// /// Creates a Percentage unit value. /// /// Percentage value (0-100) /// Additional pixel offset - public static UnitValue Percentage(double value, double offset = 0f) => new UnitValue(Units.Percentage, value, offset); - - #endregion - - #region Type Checking Properties - - /// Returns true if this value is using Auto units - public bool IsAuto => Type == Units.Auto; - - /// Returns true if this value is using Stretch units - public bool IsStretch => Type == Units.Stretch; - - /// Returns true if this value is using Pixel units - public bool IsPixels => Type == Units.Pixels; - - /// Returns true if this value is using Percentage units - public bool IsPercentage => Type == Units.Percentage; - - #endregion - - /// - /// Converts this unit value to pixels based on the parent's size. - /// - /// The parent element's size in pixels - /// Default value to use for Auto and Stretch units - /// Size in pixels - public readonly double ToPx(double parentValue, double defaultValue) - { - // Handle interpolation if active - if (_lerpData != null) - { - var startPx = _lerpData.Start.ToPx(parentValue, defaultValue); - var endPx = _lerpData.End.ToPx(parentValue, defaultValue); - return startPx + (endPx - startPx) * _lerpData.Progress; - } - - // Convert based on unit type - return Type switch { - Units.Pixels => Value, - Units.Percentage => ((Value / 100f) * parentValue) + PercentPixelOffset, - _ => defaultValue - }; - } - - /// - /// Converts this unit value to pixels and clamps it between minimum and maximum values. - /// - /// The parent element's size in pixels - /// Default value to use for Auto and Stretch units - /// Minimum allowed value - /// Maximum allowed value - /// Size in pixels, clamped between min and max - public readonly double ToPxClamped(double parentValue, double defaultValue, in UnitValue min, in UnitValue max) - { - double minValue = min.ToPx(parentValue, double.MinValue); - double maxValue = max.ToPx(parentValue, double.MaxValue); - double value = ToPx(parentValue, defaultValue); - - return Math.Min(maxValue, Math.Max(minValue, value)); - } - - /// - /// Linearly interpolates between two UnitValue instances. - /// In reality, it creates a new UnitValue with special interpolation data which is calculated when ToPx is called. - /// - /// Starting value - /// Ending value - /// Interpolation factor (0.0 to 1.0) - /// Interpolated UnitValue - public static UnitValue Lerp(in UnitValue a, in UnitValue b, double blendFactor) - { - // Ensure blend factor is between 0 and 1 - blendFactor = Math.Clamp(blendFactor, 0f, 1f); - - // If units are the same, we can blend directly - if (a.Type == b.Type) - { - return new UnitValue( - a.Type, - a.Value + (b.Value - a.Value) * blendFactor, - a.PercentPixelOffset + (b.PercentPixelOffset - a.PercentPixelOffset) * blendFactor - ); - } - - // If units are different, use interpolation data - var result = new UnitValue { - Type = a.Type, - Value = a.Value, - PercentPixelOffset = a.PercentPixelOffset, - _lerpData = new LerpData(a, b, blendFactor) - }; - return result; - } - - /// - /// Creates a deep copy of this UnitValue. - /// - /// A new UnitValue with the same properties - public readonly UnitValue Clone() => new UnitValue { - Type = Type, - Value = Value, - PercentPixelOffset = PercentPixelOffset, - _lerpData = _lerpData != null ? new LerpData(_lerpData.Start, _lerpData.End, _lerpData.Progress) : null - }; - - #region Implicit Conversions - - /// - /// Implicitly converts an integer to a pixel UnitValue. - /// - public static implicit operator UnitValue(int value) - { - return new UnitValue(Units.Pixels, value); - } - - /// - /// Implicitly converts a double to a pixel UnitValue. - /// - public static implicit operator UnitValue(double value) - { - return new UnitValue(Units.Pixels, value); - } - - #endregion - - #region Equality and Hashing - - /// - /// Compares this UnitValue with another object for equality. - /// - public override readonly bool Equals(object? obj) - { - if (obj is UnitValue other) - { - // First, check the basic properties - bool basicPropertiesEqual = Type == other.Type && - Value == other.Value && - PercentPixelOffset == other.PercentPixelOffset; - - // If either value isn't interpolating, they're equal only if both aren't - if (_lerpData is null || other._lerpData is null) - return basicPropertiesEqual && _lerpData is null && other._lerpData is null; - - // Both values are interpolating – compare their interpolation data safely - var thisLerp = _lerpData; - var otherLerp = other._lerpData; - bool lerpPropsEqual = thisLerp.Start.Equals(otherLerp.Start) && - thisLerp.End.Equals(otherLerp.End) && - thisLerp.Progress == otherLerp.Progress; - - return basicPropertiesEqual && lerpPropsEqual; - } - - return false; - } - - /// - /// Returns a hash code for this UnitValue. - /// - public override readonly int GetHashCode() => HashCode.Combine(Type, Value, PercentPixelOffset); - - #endregion - - /// - /// Returns a string representation of this UnitValue. - /// - public override readonly string ToString() => Type switch { - Units.Pixels => $"{Value}px", - Units.Percentage => $"{Value}% + {PercentPixelOffset}px", - Units.Stretch => $"Stretch({Value})", - Units.Auto => "Auto", - _ => throw new ArgumentOutOfRangeException() - }; + public static RelativeUnit Percentage(double value, double offset = 0f) => new RelativeUnit(RelativeUnits.Percentage, value, offset); } } diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 0c72af3..0b12414 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -27,6 +27,7 @@ public partial class Paper private ICanvasRenderer _renderer; private double _width; private double _height; + private ScalingSettings _scalingSettings = new ScalingSettings(); private Stopwatch _timer = new(); // Events @@ -41,6 +42,8 @@ public partial class Paper public Rect ScreenRect => new Rect(0, 0, _width, _height); public ElementHandle RootElement => _rootElementHandle; public Canvas Canvas => _canvas; + public ScalingSettings ScalingSettings => _scalingSettings; + public double ContentScale => _scalingSettings.ContentScale; /// /// Gets the current parent element in the element hierarchy. @@ -92,6 +95,14 @@ public void SetResolution(double width, double height) _height = height; } + /// + /// Sets the scaling factor applied to Points units. + /// + public void SetContentScale(double pointUnitScale) + { + _scalingSettings.ContentScale = pointUnitScale; + } + public void AddFallbackFont(FontFile font) => _canvas.AddFallbackFont(font); public IEnumerable EnumerateSystemFonts() => _canvas.EnumerateSystemFonts(); @@ -141,7 +152,7 @@ public void EndFrame() // Layout phase OnEndOfFramePreLayout?.Invoke(); - ElementLayout.Layout(_rootElementHandle, this); + ElementLayout.Layout(_rootElementHandle, this, _scalingSettings); OnEndOfFramePostLayout?.Invoke(); // Post-layout callbacks @@ -241,19 +252,24 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List 0.0f && borderColor.A > 0) { _canvas.BeginPath(); @@ -355,7 +371,7 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List state.ViewportSize.x && (flags & Scroll.ScrollX) != 0; bool hasVertical = state.ContentSize.y > state.ViewportSize.y && (flags & Scroll.ScrollY) != 0; + double borderRadius = UnitValue.Points(10).ToPx(_scalingSettings); + double scrollbarPadding = ScrollState.ScrollbarPadding.ToPx(_scalingSettings); + if (hasVertical) { - var (trackX, trackY, trackWidth, trackHeight, thumbY, thumbHeight) = state.CalculateVerticalScrollbar(rect, flags); - + var (trackX, trackY, trackWidth, trackHeight, thumbY, thumbHeight) = state.CalculateVerticalScrollbar(rect, flags, _scalingSettings); // Draw vertical scrollbar track - canvas.RoundedRectFilled(trackX, trackY, trackWidth, trackHeight, 10, 10, 10, 10, Color.FromArgb(50, 0, 0, 0)); + canvas.RoundedRectFilled( + trackX, trackY, + trackWidth, trackHeight, + borderRadius, borderRadius, borderRadius, borderRadius, + Color.FromArgb(50, 0, 0, 0)); // Draw vertical scrollbar thumb - highlight if hovered or dragging Color thumbColor = state.IsVerticalScrollbarHovered || state.IsDraggingVertical ? Color.FromArgb(220, 130, 130, 130) : Color.FromArgb(180, 100, 100, 100); - canvas.RoundedRectFilled(trackX + ScrollState.ScrollbarPadding, thumbY + ScrollState.ScrollbarPadding, trackWidth - ScrollState.ScrollbarPadding * 2, thumbHeight - ScrollState.ScrollbarPadding * 2, 10, 10, 10, 10, thumbColor); + canvas.RoundedRectFilled( + trackX + scrollbarPadding, thumbY + scrollbarPadding, + trackWidth - scrollbarPadding * 2, thumbHeight - scrollbarPadding * 2, + borderRadius, borderRadius, borderRadius, borderRadius, + thumbColor); } if (hasHorizontal) { - var (trackX, trackY, trackWidth, trackHeight, thumbX, thumbWidth) = state.CalculateHorizontalScrollbar(rect, flags); + var (trackX, trackY, trackWidth, trackHeight, thumbX, thumbWidth) = state.CalculateHorizontalScrollbar(rect, flags, _scalingSettings); // Draw horizontal scrollbar track - canvas.RoundedRectFilled(trackX, trackY, trackWidth, trackHeight, 10, 10, 10, 10, Color.FromArgb(50, 0, 0, 0)); + canvas.RoundedRectFilled( + trackX, trackY, + trackWidth, trackHeight, + borderRadius, borderRadius, borderRadius, borderRadius, + Color.FromArgb(50, 0, 0, 0)); // Draw horizontal scrollbar thumb - highlight if hovered or dragging Color thumbColor = state.IsHorizontalScrollbarHovered || state.IsDraggingHorizontal ? Color.FromArgb(220, 130, 130, 130) : Color.FromArgb(180, 100, 100, 100); - canvas.RoundedRectFilled(thumbX + ScrollState.ScrollbarPadding, trackY + ScrollState.ScrollbarPadding, thumbWidth - ScrollState.ScrollbarPadding * 2, trackHeight - ScrollState.ScrollbarPadding * 2, 10, 10, 10, 10, thumbColor); + canvas.RoundedRectFilled( + thumbX + scrollbarPadding, trackY + scrollbarPadding, + thumbWidth - scrollbarPadding * 2, trackHeight - scrollbarPadding * 2, + borderRadius, borderRadius, borderRadius, borderRadius, + thumbColor); } } @@ -612,7 +646,7 @@ internal void AddChild(ref ElementHandle handle) CurrentParent.Data.ChildIndices.Add(handle.Index); } - public void AddActionElement(Action renderAction) + public void AddActionElement(Action renderAction) { var current = CurrentParent; AddActionElement(ref current, renderAction); @@ -621,7 +655,10 @@ public void AddActionElement(Action renderAction) /// /// Adds a custom render action to an element. /// - public void AddActionElement(ref ElementHandle handle, Action renderAction) + /// + /// Canvas commands are not scaled automatically. Use the provided to apply scaling manually. + /// + public void AddActionElement(ref ElementHandle handle, Action renderAction) { ArgumentNullException.ThrowIfNull(handle); ArgumentNullException.ThrowIfNull(renderAction); @@ -712,24 +749,29 @@ private void EndOfFrameCleanupStorage() #region Layout Helpers /// - /// Creates a stretch unit value with the specified factor. + /// Creates a pixel-based unit value. /// - public UnitValue Stretch(double factor = 1f) => UnitValue.Stretch(factor); + public AbsoluteUnit Pixels(double value) => UnitValue.Pixels(value); /// - /// Creates a pixel-based unit value. + /// Creates a point-based unit value. + /// + public AbsoluteUnit Points(double value) => UnitValue.Points(value); + + /// + /// Creates a stretch unit value with the specified factor. /// - public UnitValue Pixels(double value) => UnitValue.Pixels(value); + public RelativeUnit Stretch(double factor = 1f) => UnitValue.Stretch(factor); /// /// Creates a percentage-based unit value with optional pixel offset. /// - public UnitValue Percent(double value, double pixelOffset = 0f) => UnitValue.Percentage(value, pixelOffset); + public RelativeUnit Percent(double value, double pixelOffset = 0f) => UnitValue.Percentage(value, pixelOffset); /// /// Creates an auto-sized unit value. /// - public UnitValue Auto => UnitValue.Auto; + public RelativeUnit Auto => UnitValue.Auto; #endregion } diff --git a/Paper/Paper.ElementStorage.cs b/Paper/Paper.ElementStorage.cs index 7369a8a..e5fa668 100644 --- a/Paper/Paper.ElementStorage.cs +++ b/Paper/Paper.ElementStorage.cs @@ -85,8 +85,8 @@ internal void InitializeRootElement(double width, double height) _rootElementIndex = rootHandle.Index; ref var rootData = ref rootHandle.Data; - rootData._elementStyle.SetDirectValue(GuiProp.Width, UnitValue.Pixels(width)); - rootData._elementStyle.SetDirectValue(GuiProp.Height, UnitValue.Pixels(height)); + rootData._elementStyle.SetDirectValue(GuiProp.Width, (RelativeUnit)UnitValue.Pixels(width)); + rootData._elementStyle.SetDirectValue(GuiProp.Height, (RelativeUnit)UnitValue.Pixels(height)); } internal void ClearElements() @@ -116,13 +116,13 @@ public void ValidateElementIntegrity() continue; ref var element = ref _elements[i]; - + // Validate parent-child relationships if (element.ParentIndex != -1) { if (element.ParentIndex < 0 || element.ParentIndex >= _elementCount) throw new InvalidOperationException($"Element {i} has invalid parent index {element.ParentIndex}"); - + ref var parent = ref _elements[element.ParentIndex]; if (!parent.ChildIndices.Contains(i)) throw new InvalidOperationException($"Element {i} claims parent {element.ParentIndex} but parent doesn't list it as child"); @@ -132,7 +132,7 @@ public void ValidateElementIntegrity() { if (childIndex < 0 || childIndex >= _elementCount) throw new InvalidOperationException($"Element {i} has invalid child index {childIndex}"); - + ref var child = ref _elements[childIndex]; if (child.ParentIndex != i) throw new InvalidOperationException($"Element {i} claims child {childIndex} but child doesn't reference it as parent"); diff --git a/Paper/Paper.Interaction.cs b/Paper/Paper.Interaction.cs index 162ae62..a98cd23 100644 --- a/Paper/Paper.Interaction.cs +++ b/Paper/Paper.Interaction.cs @@ -229,7 +229,7 @@ private ElementHandle FindTopmostInteractableElementForLayer(ref ElementHandle h // Calculate the combined transform Transform2D combinedTransform = parentTransform; var rect = new Rect(data.X, data.Y, data.LayoutWidth, data.LayoutHeight); - Transform2D styleTransform = data._elementStyle.GetTransformForElement(rect); + Transform2D styleTransform = data._elementStyle.GetTransformForElement(rect, _scalingSettings); combinedTransform.Premultiply(ref styleTransform); // Transform pointer position to element's local space @@ -333,7 +333,7 @@ private void BubbleEventToParents(in ElementHandle element, Action eventHandler) { ref ElementData data = ref element.Data; - + // Early exit optimization - if this element has no hooked children, skip entirely if (!data.IsAHookedParent) return; @@ -376,7 +376,7 @@ private void HandleHoverEvents(ulong previousHoveredElementId) if (data.OnLeave != null) { data.OnLeave(new ElementEvent(leftElement, data.LayoutRect, PointerPos)); - + // Propagate leave event to hooked children PropagateEventToHookedChildren(leftElement, child => { ref ElementData childData = ref child.Data; @@ -400,7 +400,7 @@ private void HandleHoverEvents(ulong previousHoveredElementId) if (!wasHovered && data.OnEnter != null) { data.OnEnter(new ElementEvent(hoveredElement, data.LayoutRect, PointerPos)); - + // Propagate enter event to hooked children PropagateEventToHookedChildren(hoveredElement, child => { ref ElementData childData = ref child.Data; @@ -410,7 +410,7 @@ private void HandleHoverEvents(ulong previousHoveredElementId) // Always trigger hover event data.OnHover?.Invoke(new ElementEvent(hoveredElement, data.LayoutRect, PointerPos)); - + // Propagate hover event to hooked children PropagateEventToHookedChildren(hoveredElement, child => { ref ElementData childData = ref child.Data; @@ -461,7 +461,7 @@ private void HandleMouseEvents() if (_focusedElementId != _activeElementId) { data.OnFocusChange?.Invoke(new FocusEvent(activeElement, true)); - + // Propagate focus gain to hooked children PropagateEventToHookedChildren(activeElement, child => { ref ElementData childData = ref child.Data; @@ -475,7 +475,7 @@ private void HandleMouseEvents() { ref ElementData oldData = ref oldFocusedElement.Data; oldData.OnFocusChange?.Invoke(new FocusEvent(oldFocusedElement, false)); - + // Propagate focus loss to hooked children PropagateEventToHookedChildren(oldFocusedElement, child => { ref ElementData childData = ref child.Data; @@ -647,13 +647,13 @@ private void HandleMouseEvents() if (!wasDragging && distanceMoved >= DRAG_THRESHOLD) { data.OnDragStart?.Invoke(new DragEvent(activeElement, layoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); - + // Propagate drag start to hooked children PropagateEventToHookedChildren(activeElement, child => { ref ElementData childData = ref child.Data; childData.OnDragStart?.Invoke(new DragEvent(child, childData.LayoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); }); - + BubbleEventToParents(activeElement, parent => { ref ElementData parentData = ref parent.Data; parentData.OnDragStart?.Invoke(new DragEvent(parent, parentData.LayoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); @@ -664,13 +664,13 @@ private void HandleMouseEvents() // Handle continuous dragging data.OnDragging?.Invoke(new DragEvent(activeElement, layoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); - + // Propagate dragging to hooked children PropagateEventToHookedChildren(activeElement, child => { ref ElementData childData = ref child.Data; childData.OnDragging?.Invoke(new DragEvent(child, childData.LayoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); }); - + BubbleEventToParents(activeElement, parent => { ref ElementData parentData = ref parent.Data; parentData.OnDragging?.Invoke(new DragEvent(parent, parentData.LayoutRect, PointerPos, startPos, PointerDelta, PointerDelta)); @@ -738,7 +738,7 @@ private void HandleTabNavigation() { // Get all elements with valid tab indices var tabbableElements = new List<(int tabIndex, ulong elementId)>(); - + // Brute force search through all elements for (int i = 0; i < _elementCount; i++) { @@ -801,7 +801,7 @@ private void HandleTabNavigation() { ref ElementData oldData = ref oldFocusedElement.Data; oldData.OnFocusChange?.Invoke(new FocusEvent(oldFocusedElement, false)); - + // Propagate focus loss to hooked children PropagateEventToHookedChildren(oldFocusedElement, child => { ref ElementData childData = ref child.Data; @@ -813,7 +813,7 @@ private void HandleTabNavigation() _focusedElementId = nextElementId; ref ElementData nextData = ref nextElement.Data; nextData.OnFocusChange?.Invoke(new FocusEvent(nextElement, true)); - + // Propagate focus gain to hooked children PropagateEventToHookedChildren(nextElement, child => { ref ElementData childData = ref child.Data; diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index d628ead..8fe6922 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -430,16 +430,16 @@ public void Update(double deltaTime) /// /// Gets the complete transform for an element. /// - public Transform2D GetTransformForElement(Rect rect) + public Transform2D GetTransformForElement(Rect rect, in ScalingSettings scalingSettings) { TransformBuilder builder = new TransformBuilder(); // Set transform properties from the current values if (_currentValues.TryGetValue(GuiProp.TranslateX, out var translateX)) - builder.SetTranslateX((double)translateX); + builder.SetTranslateX(((AbsoluteUnit)translateX).ToPx(scalingSettings)); if (_currentValues.TryGetValue(GuiProp.TranslateY, out var translateY)) - builder.SetTranslateY((double)translateY); + builder.SetTranslateY(((AbsoluteUnit)translateY).ToPx(scalingSettings)); if (_currentValues.TryGetValue(GuiProp.ScaleX, out var scaleX)) builder.SetScaleX((double)scaleX); @@ -597,9 +597,13 @@ private object Interpolate(object start, object end, double t) { return Vector4.Lerp(vector4Start, vector4End, t); } - else if (start is UnitValue unitStart && end is UnitValue unitEnd) + else if (start is RelativeUnit relativeUnitStart && end is RelativeUnit relativeUnitEnd) { - return UnitValue.Lerp(unitStart, unitEnd, t); + return RelativeUnit.Lerp(relativeUnitStart, relativeUnitEnd, t); + } + else if (start is AbsoluteUnit absoluteUnitStart && end is AbsoluteUnit absoluteUnitEnd) + { + return AbsoluteUnit.Lerp(absoluteUnitStart, absoluteUnitEnd, t); } else if (start is Transform2D transformStart && end is Transform2D transformEnd) { @@ -617,6 +621,10 @@ private object Interpolate(object start, object end, double t) { return BoxShadow.Lerp(shadowStart, shadowEnd, t); } + else if (start is Rounding roundingStart && end is Rounding roundingEnd) + { + return Rounding.Lerp(roundingStart, roundingEnd, t); + } // Default to just returning the end value return end; @@ -666,48 +674,48 @@ public static void InitializeDefaults() _defaultValues[(int)GuiProp.BackgroundColor] = Color.Transparent; _defaultValues[(int)GuiProp.BackgroundGradient] = Gradient.None; _defaultValues[(int)GuiProp.BorderColor] = Color.Transparent; - _defaultValues[(int)GuiProp.BorderWidth] = 0.0; - _defaultValues[(int)GuiProp.Rounded] = new Vector4(0, 0, 0, 0); + _defaultValues[(int)GuiProp.BorderWidth] = (AbsoluteUnit)UnitValue.Points(0); + _defaultValues[(int)GuiProp.Rounded] = new Rounding(0, 0, 0, 0); _defaultValues[(int)GuiProp.BoxShadow] = BoxShadow.None; // Core Layout Properties _defaultValues[(int)GuiProp.AspectRatio] = -1.0; - _defaultValues[(int)GuiProp.Width] = UnitValue.Stretch(); - _defaultValues[(int)GuiProp.Height] = UnitValue.Stretch(); - _defaultValues[(int)GuiProp.MinWidth] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxWidth] = UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinHeight] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxHeight] = UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.Width] = (RelativeUnit)UnitValue.Stretch(); + _defaultValues[(int)GuiProp.Height] = (RelativeUnit)UnitValue.Stretch(); + _defaultValues[(int)GuiProp.MinWidth] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxWidth] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinHeight] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxHeight] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); // Positioning Properties - _defaultValues[(int)GuiProp.Left] = UnitValue.Auto; - _defaultValues[(int)GuiProp.Right] = UnitValue.Auto; - _defaultValues[(int)GuiProp.Top] = UnitValue.Auto; - _defaultValues[(int)GuiProp.Bottom] = UnitValue.Auto; - _defaultValues[(int)GuiProp.MinLeft] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxLeft] = UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinRight] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxRight] = UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinTop] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxTop] = UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinBottom] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxBottom] = UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.Left] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.Right] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.Top] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.Bottom] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.MinLeft] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxLeft] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinRight] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxRight] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinTop] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxTop] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinBottom] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxBottom] = (RelativeUnit)UnitValue.Pixels(double.MaxValue); // Child Layout Properties - _defaultValues[(int)GuiProp.ChildLeft] = UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildRight] = UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildTop] = UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildBottom] = UnitValue.Auto; - _defaultValues[(int)GuiProp.RowBetween] = UnitValue.Auto; - _defaultValues[(int)GuiProp.ColBetween] = UnitValue.Auto; - _defaultValues[(int)GuiProp.BorderLeft] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderRight] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderTop] = UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderBottom] = UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.ChildLeft] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildRight] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildTop] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildBottom] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.RowBetween] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.ColBetween] = (RelativeUnit)UnitValue.Auto; + _defaultValues[(int)GuiProp.BorderLeft] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderRight] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderTop] = (RelativeUnit)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderBottom] = (RelativeUnit)UnitValue.Pixels(0); // Transform Properties - _defaultValues[(int)GuiProp.TranslateX] = 0.0; - _defaultValues[(int)GuiProp.TranslateY] = 0.0; + _defaultValues[(int)GuiProp.TranslateX] = (AbsoluteUnit)UnitValue.Points(0); + _defaultValues[(int)GuiProp.TranslateY] = (AbsoluteUnit)UnitValue.Points(0); _defaultValues[(int)GuiProp.ScaleX] = 1.0; _defaultValues[(int)GuiProp.ScaleY] = 1.0; _defaultValues[(int)GuiProp.Rotate] = 0.0; @@ -719,11 +727,11 @@ public static void InitializeDefaults() // Text Properties _defaultValues[(int)GuiProp.TextColor] = Color.White; - _defaultValues[(int)GuiProp.WordSpacing] = 0.0; - _defaultValues[(int)GuiProp.LetterSpacing] = 0.0; + _defaultValues[(int)GuiProp.WordSpacing] = (AbsoluteUnit)UnitValue.Points(0); + _defaultValues[(int)GuiProp.LetterSpacing] = (AbsoluteUnit)UnitValue.Points(0); _defaultValues[(int)GuiProp.LineHeight] = 1.0; _defaultValues[(int)GuiProp.TabSize] = 4; - _defaultValues[(int)GuiProp.FontSize] = 16.0; + _defaultValues[(int)GuiProp.FontSize] = (AbsoluteUnit)UnitValue.Points(16); _initialized = true; } diff --git a/Paper/Rounding.cs b/Paper/Rounding.cs new file mode 100644 index 0000000..039f5db --- /dev/null +++ b/Paper/Rounding.cs @@ -0,0 +1,50 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +using Prowl.PaperUI.LayoutEngine; +using Prowl.Vector; + +namespace Prowl.PaperUI +{ + /// + /// Defines how much rounding is applied to an element. + /// + public struct Rounding + { + public AbsoluteUnit TopLeft; + public AbsoluteUnit TopRight; + public AbsoluteUnit BottomRight; + public AbsoluteUnit BottomLeft; + + public Rounding(in AbsoluteUnit topLeft, in AbsoluteUnit topRight, in AbsoluteUnit bottomRight, in AbsoluteUnit bottomLeft) + { + TopLeft = topLeft; + TopRight = topRight; + BottomRight = bottomRight; + BottomLeft = bottomLeft; + } + + public Vector4 ToPx(in ScalingSettings scalingSettings) + { + return new Vector4( + TopLeft.ToPx(scalingSettings), TopRight.ToPx(scalingSettings), + BottomRight.ToPx(scalingSettings), BottomLeft.ToPx(scalingSettings)); + } + + /// + /// Linearly interpolates between two Rounding instances. + /// + /// Starting value + /// Ending value + /// Interpolation factor (0.0 to 1.0) + /// Interpolated Rounding + public static Rounding Lerp(in Rounding a, in Rounding b, double blendFactor) + { + return new Rounding( + AbsoluteUnit.Lerp(a.TopLeft, b.TopLeft, blendFactor), + AbsoluteUnit.Lerp(a.TopRight, b.TopRight, blendFactor), + AbsoluteUnit.Lerp(a.BottomRight, b.BottomRight, blendFactor), + AbsoluteUnit.Lerp(a.BottomLeft, b.BottomLeft, blendFactor)); + } + } +} diff --git a/Paper/ScrollState.cs b/Paper/ScrollState.cs index b9e82b3..909d316 100644 --- a/Paper/ScrollState.cs +++ b/Paper/ScrollState.cs @@ -1,4 +1,5 @@ -using Prowl.Vector; +using Prowl.PaperUI.LayoutEngine; +using Prowl.Vector; namespace Prowl.PaperUI { @@ -23,9 +24,9 @@ public struct ScrollState public bool IsHorizontalScrollbarHovered; // Constants for scrollbar rendering - public const double ScrollbarSize = 12; - public const double ScrollbarMinSize = 20; - public const double ScrollbarPadding = 2; + public static readonly AbsoluteUnit ScrollbarSize = 12; + public static readonly AbsoluteUnit ScrollbarMinSize = 20; + public static readonly AbsoluteUnit ScrollbarPadding = 2; /// /// Gets the maximum scroll position. @@ -67,64 +68,68 @@ public void ClampScrollPosition() /// /// Calculates the vertical scrollbar dimensions based on the element rect. /// - public (double x, double y, double width, double height, double thumbY, double thumbHeight) CalculateVerticalScrollbar(Rect rect, Scroll flags) + public (double x, double y, double width, double height, double thumbY, double thumbHeight) CalculateVerticalScrollbar(Rect rect, Scroll flags, in ScalingSettings scalingSettings) { bool hasHorizontal = NeedsHorizontalScroll(flags); + double scrollbarSize = ScrollbarSize.ToPx(scalingSettings); + double scrollbarMinSize = ScrollbarMinSize.ToPx(scalingSettings); // Calculate track dimensions double trackHeight = rect.height; if (hasHorizontal) - trackHeight -= ScrollbarSize; + trackHeight -= scrollbarSize; - double trackX = rect.x + rect.width - ScrollbarSize; + double trackX = rect.x + rect.width - scrollbarSize; double trackY = rect.y; // Calculate thumb dimensions - double thumbHeight = Math.Max(ScrollbarMinSize, + double thumbHeight = Math.Max(scrollbarMinSize, (ViewportSize.y / ContentSize.y) * trackHeight); double thumbY = trackY; if (MaxScroll.y > 0) thumbY += (Position.y / MaxScroll.y) * (trackHeight - thumbHeight); - return (trackX, trackY, ScrollbarSize, trackHeight, thumbY, thumbHeight); + return (trackX, trackY, scrollbarSize, trackHeight, thumbY, thumbHeight); } /// /// Calculates the horizontal scrollbar dimensions based on the element rect. /// - public (double x, double y, double width, double height, double thumbX, double thumbWidth) CalculateHorizontalScrollbar(Rect rect, Scroll flags) + public (double x, double y, double width, double height, double thumbX, double thumbWidth) CalculateHorizontalScrollbar(Rect rect, Scroll flags, in ScalingSettings scalingSettings) { bool hasVertical = NeedsVerticalScroll(flags); + double scrollbarSize = ScrollbarSize.ToPx(scalingSettings); + double scrollbarMinSize = ScrollbarMinSize.ToPx(scalingSettings); // Calculate track dimensions double trackWidth = rect.width; if (hasVertical) - trackWidth -= ScrollbarSize; + trackWidth -= scrollbarSize; double trackX = rect.x; - double trackY = rect.y + rect.height - ScrollbarSize; + double trackY = rect.y + rect.height - scrollbarSize; // Calculate thumb dimensions - double thumbWidth = Math.Max(ScrollbarMinSize, + double thumbWidth = Math.Max(scrollbarMinSize, (ViewportSize.x / ContentSize.x) * trackWidth); double thumbX = trackX; if (MaxScroll.x > 0) thumbX += (Position.x / MaxScroll.x) * (trackWidth - thumbWidth); - return (trackX, trackY, trackWidth, ScrollbarSize, thumbX, thumbWidth); + return (trackX, trackY, trackWidth, scrollbarSize, thumbX, thumbWidth); } /// /// Checks if a point is over the vertical scrollbar. /// - public bool IsPointOverVerticalScrollbar(Vector2 point, Rect rect, Scroll flags) + public bool IsPointOverVerticalScrollbar(Vector2 point, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!NeedsVerticalScroll(flags)) return false; - var (trackX, trackY, trackWidth, trackHeight, _, _) = CalculateVerticalScrollbar(rect, flags); + var (trackX, trackY, trackWidth, trackHeight, _, _) = CalculateVerticalScrollbar(rect, flags, scalingSettings); return point.x >= trackX && point.x <= trackX + trackWidth && @@ -135,12 +140,12 @@ public bool IsPointOverVerticalScrollbar(Vector2 point, Rect rect, Scroll flags) /// /// Checks if a point is over the horizontal scrollbar. /// - public bool IsPointOverHorizontalScrollbar(Vector2 point, Rect rect, Scroll flags) + public bool IsPointOverHorizontalScrollbar(Vector2 point, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!NeedsHorizontalScroll(flags)) return false; - var (trackX, trackY, trackWidth, trackHeight, _, _) = CalculateHorizontalScrollbar(rect, flags); + var (trackX, trackY, trackWidth, trackHeight, _, _) = CalculateHorizontalScrollbar(rect, flags, scalingSettings); return point.x >= trackX && point.x <= trackX + trackWidth && @@ -151,12 +156,12 @@ public bool IsPointOverHorizontalScrollbar(Vector2 point, Rect rect, Scroll flag /// /// Handles scrollbar dragging for vertical scrollbar. /// - public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags) + public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!IsDraggingVertical) return; - var (_, trackY, _, trackHeight, _, thumbHeight) = CalculateVerticalScrollbar(rect, flags); + var (_, trackY, _, trackHeight, _, thumbHeight) = CalculateVerticalScrollbar(rect, flags, scalingSettings); double dragDelta = mousePos.y - DragStartPosition.y; double scrollableHeight = trackHeight - thumbHeight; @@ -176,12 +181,12 @@ public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flag /// /// Handles scrollbar dragging for horizontal scrollbar. /// - public void HandleHorizontalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags) + public void HandleHorizontalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!IsDraggingHorizontal) return; - var (trackX, _, trackWidth, _, _, thumbWidth) = CalculateHorizontalScrollbar(rect, flags); + var (trackX, _, trackWidth, _, _, thumbWidth) = CalculateHorizontalScrollbar(rect, flags, scalingSettings); double dragDelta = mousePos.x - DragStartPosition.x; double scrollableWidth = trackWidth - thumbWidth; diff --git a/README.md b/README.md index 9abb985..a86b9a5 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ For a complete guide, tutorials, and API reference, please visit the **[Official - Fluent API - Flexible Layout System - Rows, Columns & Custom Positioning - - Pixel, Percentage, Stretch or Auto for Positioning and Sizing + - Pixels, Points, Percentage, Stretch or Auto for Positioning and Sizing + - Content Scaling - A Powerful Built-in Animation System - Many built-in Easing functions - Easily provide your own Easing functions @@ -101,7 +102,7 @@ For a complete guide, tutorials, and API reference, please visit the **[Official - MoveTo, LineTo, CurveTo, Fill, Stroke, etc - Box Shadows - Linear, Radial and Box Gradients - - Draw custom shapes at any time any where + - Draw custom shapes at any time anywhere

(back to top)

@@ -134,7 +135,7 @@ void RenderUI() { // Begin the UI frame Paper.BeginFrame(deltaTime); - + // Define your UI using (Paper.Column("MainContainer") .BackgroundColor(240, 240, 240) @@ -146,7 +147,7 @@ void RenderUI() .BackgroundColor(50, 120, 200) .Text(Text.Center("My Application", myFont, Color.White)) .Enter()) { } - + // Content area using (Paper.Row("Content").Enter()) { @@ -154,12 +155,12 @@ void RenderUI() Paper.Box("Sidebar") .Width(200) .BackgroundColor(220, 220, 220); - + // Main content Paper.Box("MainContent"); } } - + // End the UI frame Paper.EndFrame(); } @@ -261,7 +262,7 @@ Paper.Box("InteractiveElement") .OnScroll((delta, rect) => Scroll(delta)) ``` ## Input Handling -To integrate Paper's input system with your project, you need to forward input events from your project to PaperUI. +To integrate Paper's input system with your project, you need to forward input events from your project to PaperUI. Here's a simplified example using Raylib: ```cs @@ -270,19 +271,19 @@ void UpdatePaperUIInput() { // Update mouse position Paper.SetPointerPosition(mousePos); - + // Forward mouse button events if (IsMouseButtonPressed(MouseButton.Left)) Paper.SetPointerState(PaperMouseBtn.Left, mousePos, true); if (IsMouseButtonReleased(MouseButton.Left)) Paper.SetPointerState(PaperMouseBtn.Left, mousePos, false); // Repeat for Right & Middle - + // Forward mouse wheel events float wheelDelta = GetMouseWheelMove(); if (wheelDelta != 0) Paper.SetPointerWheel(wheelDelta); - + // Forward text input int key = GetCharPressed(); while (key > 0) @@ -290,7 +291,7 @@ void UpdatePaperUIInput() Paper.AddInputCharacter(((char)key).ToString()); key = GetCharPressed(); } - + // Forward key states // keyMappings being an array storing the mapping from a PaperKey enum to your Projects Key Enum foreach (var keyMapping in keyMappings) diff --git a/Samples/Shared/PaperDemo.Components.cs b/Samples/Shared/PaperDemo.Components.cs index b24d9e0..e6c1f8d 100644 --- a/Samples/Shared/PaperDemo.Components.cs +++ b/Samples/Shared/PaperDemo.Components.cs @@ -377,7 +377,7 @@ public static void DefineStyles() .Rounded(20) .BackgroundColor(Color.White) //.PositionType(PositionType.SelfDirected) - .Top(PaperDemo.Gui.Pixels(3)) + .Top(3) .Transition(GuiProp.Left, 0.25, Easing.CubicInOut)); } @@ -392,10 +392,10 @@ public static ElementBuilder Primary(string id, bool isOn) .Style("toggle") .StyleIf(isOn, "toggle-on") .StyleIf(!isOn, "toggle-off")).Enter()) - { + { PaperDemo.Gui.Box($"ToggleDot{id}") .Style("toggle-dot") - .Left(PaperDemo.Gui.Pixels(isOn ? 32 : 4)); + .Left(isOn ? 32 : 4); } return builder; } @@ -412,7 +412,7 @@ public static ElementBuilder Primary(string id, double[] values, double startAng { // Add a simple pie chart visualization - PaperDemo.Gui.AddActionElement((vg, rect) => + PaperDemo.Gui.AddActionElement((vg, rect, scalingSettings) => { double centerX = rect.x + rect.width / 2; double centerY = rect.y + rect.height / 2; @@ -461,7 +461,7 @@ public static ElementBuilder Primary(string id, double[] values, double startAng //vg.TextAlign(Align.Center | Align.Middle); //vg.FontSize(16); //vg.Text(labelX, labelY, label); - vg.DrawText(label, labelX, labelY, Color.White, 18, Fonts.arial); + vg.DrawText(label, labelX, labelY, Color.White, UnitValue.Points(18).ToPx(scalingSettings), Fonts.arial); // Move to next slice startAngle = endAngle; diff --git a/Samples/Shared/PaperDemo.cs b/Samples/Shared/PaperDemo.cs index 8322d72..b8c2dd4 100644 --- a/Samples/Shared/PaperDemo.cs +++ b/Samples/Shared/PaperDemo.cs @@ -1,6 +1,7 @@ using System.Drawing; using Prowl.PaperUI; +using Prowl.PaperUI.LayoutEngine; using Prowl.PaperUI.Themes.Origami; using Prowl.Vector; @@ -426,8 +427,8 @@ private static void RenderDashboardTab() .Enter()) { // Draw a simple chart with animated data - Gui.AddActionElement((vg, rect) => { - + Gui.AddActionElement((vg, rect, scalingSettings) => + { // Draw grid lines for (int i = 0; i <= 5; i++) { @@ -436,7 +437,7 @@ private static void RenderDashboardTab() vg.MoveTo(rect.x, y); vg.LineTo(rect.x + rect.width, y); vg.SetStrokeColor(Themes.lightTextColor); - vg.SetStrokeWidth(1); + vg.SetStrokeWidth(UnitValue.Points(1).ToPx(scalingSettings)); vg.Stroke(); } @@ -499,7 +500,7 @@ private static void RenderDashboardTab() } vg.SetStrokeColor(Themes.primaryColor); - vg.SetStrokeWidth(3); + vg.SetStrokeWidth(UnitValue.Points(3).ToPx(scalingSettings)); vg.Stroke(); // Draw points @@ -513,12 +514,12 @@ private static void RenderDashboardTab() double y = rect.y + rect.height - (animatedValue * rect.height); vg.BeginPath(); - vg.Circle(x, y, 6); + vg.Circle(x, y, UnitValue.Points(6).ToPx(scalingSettings)); vg.SetFillColor(Color.White); vg.Fill(); vg.BeginPath(); - vg.Circle(x, y, 4); + vg.Circle(x, y, UnitValue.Points(4).ToPx(scalingSettings)); vg.SetFillColor(Themes.primaryColor); vg.Fill(); } @@ -898,7 +899,7 @@ private static void RenderProfileTab() .Enter()) { // Render contribution graph - Gui.AddActionElement((vg, rect) => { + Gui.AddActionElement((vg, rect, scalingSettings) => { int days = 7; int weeks = 4; double cellWidth = rect.width / days; @@ -918,9 +919,11 @@ private static void RenderProfileTab() double value = Math.Sin(week * 0.4f + day * 0.7f + time) * 0.5f + 0.5f; value = Math.Pow(value, 1.5f); + double borderRadius = UnitValue.Points(3).ToPx(scalingSettings); + // Draw cell vg.BeginPath(); - vg.RoundedRect(x, y, cellSize, cellSize, 3, 3, 3, 3); + vg.RoundedRect(x, y, cellSize, cellSize, borderRadius, borderRadius, borderRadius, borderRadius); // Apply color based on intensity int alpha = (int)(40 + value * 215); diff --git a/Tests/AbsoluteUnitTests.cs b/Tests/AbsoluteUnitTests.cs new file mode 100644 index 0000000..887742f --- /dev/null +++ b/Tests/AbsoluteUnitTests.cs @@ -0,0 +1,61 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +using Prowl.PaperUI; +using Prowl.PaperUI.LayoutEngine; + +namespace Tests; + +public class AbsoluteUnitTests +{ + [Fact] + public void Equals_BasicProperties_ReturnsTrue() + { + var a = new AbsoluteUnit(AbsoluteUnits.Pixels, 10); + var b = new AbsoluteUnit(AbsoluteUnits.Pixels, 10); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + } + + [Fact] + public void Equals_InterpolatingVsNonInterpolating_ReturnsFalse() + { + var interpolating = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Pixels, 30), 0.5); + var plain = new AbsoluteUnit(AbsoluteUnits.Pixels, 10); + + Assert.False(interpolating.Equals(plain)); + Assert.False(plain.Equals(interpolating)); + } + + [Fact] + public void Equals_InterpolatingBothWithSameData_ReturnsTrue() + { + var first = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Pixels, 30), 0.5); + var second = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Pixels, 30), 0.5); + + Assert.True(first.Equals(second)); + Assert.True(second.Equals(first)); + } + + [Fact] + public void Equals_InterpolatingDifferentProgress_ReturnsFalse() + { + var first = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Pixels, 30), 0.5); + var second = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Pixels, 30), 0.25); + + Assert.False(first.Equals(second)); + Assert.False(second.Equals(first)); + } + + [Fact] + public void ToPx_WithInterpolation_ComputesExpectedValue() + { + var scalingSettings = new ScalingSettings(3); + var uv = AbsoluteUnit.Lerp(new AbsoluteUnit(AbsoluteUnits.Pixels, 10), new AbsoluteUnit(AbsoluteUnits.Points, 50), 0.5); + + double result = uv.ToPx(scalingSettings); + + Assert.Equal(80, result, 5); + } +} diff --git a/Tests/RelativeUnitTests.cs b/Tests/RelativeUnitTests.cs new file mode 100644 index 0000000..5b25346 --- /dev/null +++ b/Tests/RelativeUnitTests.cs @@ -0,0 +1,61 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +using Prowl.PaperUI; +using Prowl.PaperUI.LayoutEngine; + +namespace Tests; + +public class RelativeUnitTests +{ + [Fact] + public void Equals_BasicProperties_ReturnsTrue() + { + var a = new RelativeUnit(RelativeUnits.Pixels, 10); + var b = new RelativeUnit(RelativeUnits.Pixels, 10); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + } + + [Fact] + public void Equals_InterpolatingVsNonInterpolating_ReturnsFalse() + { + var interpolating = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.5); + var plain = new RelativeUnit(RelativeUnits.Pixels, 10); + + Assert.False(interpolating.Equals(plain)); + Assert.False(plain.Equals(interpolating)); + } + + [Fact] + public void Equals_InterpolatingBothWithSameData_ReturnsTrue() + { + var first = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.5); + var second = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.5); + + Assert.True(first.Equals(second)); + Assert.True(second.Equals(first)); + } + + [Fact] + public void Equals_InterpolatingDifferentProgress_ReturnsFalse() + { + var first = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.5); + var second = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.25); + + Assert.False(first.Equals(second)); + Assert.False(second.Equals(first)); + } + + [Fact] + public void ToPx_WithInterpolation_ComputesExpectedValue() + { + var scalingSettings = new ScalingSettings(); + var uv = RelativeUnit.Lerp(new RelativeUnit(RelativeUnits.Pixels, 10), new RelativeUnit(RelativeUnits.Percentage, 50), 0.5); + + double result = uv.ToPx(200, 0, scalingSettings); + + Assert.Equal(55, result, 5); + } +} diff --git a/Tests/UnitValueTests.cs b/Tests/UnitValueTests.cs deleted file mode 100644 index f303e74..0000000 --- a/Tests/UnitValueTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// This file is part of the Prowl Game Engine -// Licensed under the MIT License. See the LICENSE file in the project root for details. - -using Prowl.PaperUI.LayoutEngine; - -namespace Tests; - -public class UnitValueTests -{ - [Fact] - public void Equals_BasicProperties_ReturnsTrue() - { - var a = UnitValue.Pixels(10); - var b = UnitValue.Pixels(10); - - Assert.True(a.Equals(b)); - Assert.True(b.Equals(a)); - } - - [Fact] - public void Equals_InterpolatingVsNonInterpolating_ReturnsFalse() - { - var interpolating = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.5); - var plain = UnitValue.Pixels(10); - - Assert.False(interpolating.Equals(plain)); - Assert.False(plain.Equals(interpolating)); - } - - [Fact] - public void Equals_InterpolatingBothWithSameData_ReturnsTrue() - { - var first = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.5); - var second = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.5); - - Assert.True(first.Equals(second)); - Assert.True(second.Equals(first)); - } - - [Fact] - public void Equals_InterpolatingDifferentProgress_ReturnsFalse() - { - var first = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.5); - var second = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.25); - - Assert.False(first.Equals(second)); - Assert.False(second.Equals(first)); - } - - [Fact] - public void ToPx_WithInterpolation_ComputesExpectedValue() - { - var uv = UnitValue.Lerp(UnitValue.Pixels(10), UnitValue.Percentage(50), 0.5); - - double result = uv.ToPx(200, 0); - - Assert.Equal(55, result, 5); - } -}