From 6e90b70c5168d6c0d4d145cf996f13e9763b3055 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 09:47:05 -0400 Subject: [PATCH 01/38] Split UnitValue into two structs: Relative and AbsoluteSize --- Paper/ElementBuilder.cs | 304 +++++++++--------- Paper/Enums.cs | 26 -- Paper/LayoutEngine/ElementLayout.cs | 160 +++++----- Paper/LayoutEngine/RelativeSize.cs | 469 ++++++++++++++++++++++++++++ Paper/LayoutEngine/UnitValue.cs | 257 --------------- Paper/Paper.Core.cs | 8 +- Paper/Paper.ElementStorage.cs | 6 +- Paper/Paper.Styles.cs | 4 +- 8 files changed, 710 insertions(+), 524 deletions(-) create mode 100644 Paper/LayoutEngine/RelativeSize.cs delete mode 100644 Paper/LayoutEngine/UnitValue.cs diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index b06f6b7..1825255 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -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 RelativeSize 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 RelativeSize width, in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize left, in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize all) => Margin(all, all, all, all); /// Sets horizontal and vertical margins. - public T Margin(in UnitValue horizontal, in UnitValue vertical) => + public T Margin(in RelativeSize horizontal, in RelativeSize 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 RelativeSize left, in RelativeSize right, in RelativeSize top, in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize colBetween) => SetStyleProperty(GuiProp.ColBetween, colBetween); /// Sets the left border width. - public T BorderLeft(in UnitValue borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); + public T BorderLeft(in RelativeSize borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); /// Sets the right border width. - public T BorderRight(in UnitValue borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); + public T BorderRight(in RelativeSize borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); /// Sets the top border width. - public T BorderTop(in UnitValue borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); + public T BorderTop(in RelativeSize borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); /// Sets the bottom border width. - public T BorderBottom(in UnitValue borderBottom) => SetStyleProperty(GuiProp.BorderBottom, borderBottom); + public T BorderBottom(in RelativeSize 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 RelativeSize 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 RelativeSize horizontal, in RelativeSize 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 RelativeSize left, in RelativeSize right, in RelativeSize top, in RelativeSize bottom) { SetStyleProperty(GuiProp.BorderLeft, left); SetStyleProperty(GuiProp.BorderRight, right); @@ -628,19 +628,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 +895,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; @@ -1201,19 +1201,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 +1249,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 +1286,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,19 +1331,19 @@ 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); @@ -1356,10 +1356,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 +1369,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 +1393,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 +1414,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 +1458,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 +1475,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 +1491,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.Delete: if (state.HasSelection) { @@ -1504,7 +1504,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.Left: if (IsControlPressed()) { @@ -1537,7 +1537,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.Right: if (IsControlPressed()) { @@ -1570,7 +1570,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.Home: if (IsShiftPressed()) { @@ -1584,7 +1584,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput state.ClearSelection(); } break; - + case PaperKey.End: if (IsShiftPressed()) { @@ -1598,13 +1598,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 +1612,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 +1622,7 @@ private bool ProcessKeyCommand(ref TextInputState state, PaperKey key, TextInput valueChanged = true; } break; - + case PaperKey.V when IsControlPressed(): { string clipText = _paper.GetClipboard(); @@ -1631,7 +1631,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 +1644,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 +1661,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 +1671,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 +1684,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 +1740,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 +1787,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 +1827,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 +1865,7 @@ private ElementBuilder CreateTextInput( currentState.CursorPosition = newPosition; currentState.ClearSelection(); } - + EnsureCursorVisible(ref currentState, settings, isMultiLine); SaveTextInputState(currentState); }); @@ -1891,7 +1891,7 @@ private ElementBuilder CreateTextInput( SaveTextInputState(currentState); } }); - + // Handle dragging for text selection OnDragStart((DragEvent e) => { @@ -1899,28 +1899,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; - + 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 +1928,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); @@ -1992,7 +1992,7 @@ private ElementBuilder CreateTextInput( { var renderState = LoadTextInputState(value, isMultiLine); var layoutSettings = CreateTextLayoutSettings(settings, isMultiLine, r.width); - + canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); @@ -2007,37 +2007,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 +2046,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 +2065,7 @@ private ElementBuilder CreateTextInput( currentY += lineHeight; currentLineIndex++; } - + // Last line: from start of line to end position if (endPos.X > 0) { @@ -2084,15 +2084,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 +2100,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 +2109,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 +2129,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; - + // 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); @@ -2156,10 +2156,10 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set var fontSize = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); - + double visibleWidth = _handle.Data.LayoutWidth; const double margin = 20.0; - + if (cursorPos.x < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.x - margin); else if (cursorPos.x > state.ScrollOffsetX + visibleWidth - margin) @@ -2170,7 +2170,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 +2181,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 +2196,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/Enums.cs b/Paper/Enums.cs index 2af80c3..77e1931 100644 --- a/Paper/Enums.cs +++ b/Paper/Enums.cs @@ -34,32 +34,6 @@ public enum PositionType ParentDirected } - /// - /// Defines measurement units for element dimensions and positioning. - /// - public enum Units - { - /// - /// Fixed pixel measurements. - /// - Pixels, - - /// - /// Percentage of parent container's corresponding dimension. - /// - Percentage, - - /// - /// Flexible sizing that distributes available space based on stretch factors. - /// - Stretch, - - /// - /// Size is determined automatically based on content or other constraints. - /// - Auto - } - public enum TextAlignment { Left, diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 3f5b8aa..6ee9223 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -13,8 +13,8 @@ internal static UISize Layout(ElementHandle elementHandle, Paper gui) { ref var data = ref elementHandle.Data; - var wValue = (UnitValue)data._elementStyle.GetValue(GuiProp.Width); - var hValue = (UnitValue)data._elementStyle.GetValue(GuiProp.Height); + var wValue = (RelativeSize)data._elementStyle.GetValue(GuiProp.Width); + var hValue = (RelativeSize)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"); @@ -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 RelativeSize GetProp(ref ElementData element, LayoutType parentType, GuiProp row, GuiProp column) + => (RelativeSize)element._elementStyle.GetValue(parentType == LayoutType.Row ? row : column); + + private static RelativeSize GetMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Width, GuiProp.Height); + private static RelativeSize GetCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Height, GuiProp.Width); + private static RelativeSize GetMinMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinWidth, GuiProp.MinHeight); + private static RelativeSize GetMaxMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxWidth, GuiProp.MaxHeight); + private static RelativeSize GetMinCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinHeight, GuiProp.MinWidth); + private static RelativeSize GetMaxCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxHeight, GuiProp.MaxWidth); + private static RelativeSize GetMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Left, GuiProp.Top); + private static RelativeSize GetMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Right, GuiProp.Bottom); + private static RelativeSize GetCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Top, GuiProp.Left); + private static RelativeSize GetCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Bottom, GuiProp.Right); + private static RelativeSize GetChildMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildLeft, GuiProp.ChildTop); + private static RelativeSize GetChildMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildRight, GuiProp.ChildBottom); + private static RelativeSize GetChildCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildTop, GuiProp.ChildLeft); + private static RelativeSize 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 RelativeSize GetMainBetween(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.RowBetween, GuiProp.ColBetween); + private static RelativeSize GetMinMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinLeft, GuiProp.MinTop); + private static RelativeSize GetMaxMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxLeft, GuiProp.MaxTop); + private static RelativeSize GetMinMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinRight, GuiProp.MinBottom); + private static RelativeSize GetMaxMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxRight, GuiProp.MaxBottom); + private static RelativeSize GetMinCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinTop, GuiProp.MinLeft); + private static RelativeSize GetMaxCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxTop, GuiProp.MaxLeft); + private static RelativeSize GetMinCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinBottom, GuiProp.MinRight); + private static RelativeSize GetMaxCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxBottom, GuiProp.MaxRight); + private static RelativeSize GetBorderMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderLeft, GuiProp.BorderTop); + private static RelativeSize GetBorderMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderRight, GuiProp.BorderBottom); + private static RelativeSize GetBorderCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderTop, GuiProp.BorderLeft); + private static RelativeSize 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) { @@ -124,8 +124,8 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay ref var element = ref elementHandle.Data; LayoutType layoutType = element.LayoutType; - UnitValue main = GetMain(ref element, parentLayoutType); - UnitValue cross = GetCross(ref element, parentLayoutType); + RelativeSize main = GetMain(ref element, parentLayoutType); + RelativeSize cross = GetCross(ref element, parentLayoutType); double minMain = main.IsStretch ? DEFAULT_MIN @@ -218,7 +218,7 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay // 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); + RelativeSize elementChildMainBefore = GetChildMainBefore(ref element, layoutType); + RelativeSize elementChildMainAfter = GetChildMainAfter(ref element, layoutType); + RelativeSize elementChildCrossBefore = GetChildCrossBefore(ref element, layoutType); + RelativeSize elementChildCrossAfter = GetChildCrossAfter(ref element, layoutType); + RelativeSize 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); + RelativeSize childMainBefore = GetMainBefore(ref child, layoutType); + RelativeSize childMain = GetMain(ref child, layoutType); + RelativeSize childMainAfter = GetMainAfter(ref child, layoutType); - UnitValue childCrossBefore = GetCrossBefore(ref child, layoutType); - UnitValue childCross = GetCross(ref child, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child, layoutType); + RelativeSize childCrossBefore = GetCrossBefore(ref child, layoutType); + RelativeSize childCross = GetCross(ref child, layoutType); + RelativeSize 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); + RelativeSize childMinCrossBefore = GetMinCrossBefore(ref child, layoutType); + RelativeSize childMaxCrossBefore = GetMaxCrossBefore(ref child, layoutType); + RelativeSize childMinCrossAfter = GetMinCrossAfter(ref child, layoutType); + RelativeSize childMaxCrossAfter = GetMaxCrossAfter(ref child, layoutType); + RelativeSize childMinMainBefore = GetMinMainBefore(ref child, layoutType); + RelativeSize childMaxMainBefore = GetMaxMainBefore(ref child, layoutType); + RelativeSize childMinMainAfter = GetMinMainAfter(ref child, layoutType); + RelativeSize childMaxMainAfter = GetMaxMainAfter(ref child, layoutType); + RelativeSize childMinMain = GetMinMain(ref child, layoutType); + RelativeSize childMaxMain = GetMaxMain(ref child, layoutType); // Apply parent overrides to auto spacing if (childMainBefore.IsAuto && first == i) @@ -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); + RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeSize childCross = GetCross(ref child.Element.Data, layoutType); + RelativeSize childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); if (childCrossBefore.IsAuto) childCrossBefore = elementChildCrossBefore; @@ -771,12 +771,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); + RelativeSize childMainBefore = GetMainBefore(ref childHandle.Data, layoutType); + RelativeSize childMain = GetMain(ref childHandle.Data, layoutType); + RelativeSize childMainAfter = GetMainAfter(ref childHandle.Data, layoutType); + RelativeSize childCrossBefore = GetCrossBefore(ref childHandle.Data, layoutType); + RelativeSize childCross = GetCross(ref childHandle.Data, layoutType); + RelativeSize childCrossAfter = GetCrossAfter(ref childHandle.Data, layoutType); // Apply parent overrides if (childMainBefore.IsAuto) @@ -917,13 +917,13 @@ private static void ProcessChildCrossStretching( double parentMain, double borderCrossBefore, double borderCrossAfter, - UnitValue elementChildCrossBefore, - UnitValue elementChildCrossAfter, + RelativeSize elementChildCrossBefore, + RelativeSize 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); + RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeSize childCross = GetCross(ref child.Element.Data, layoutType); + RelativeSize childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childCrossBefore.IsAuto) @@ -1037,13 +1037,13 @@ private static void ProcessChildMainStretching( double parentCross, double borderMainBefore, double borderMainAfter, - UnitValue elementChildMainBefore, - UnitValue elementChildMainAfter, + RelativeSize elementChildMainBefore, + RelativeSize 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); + RelativeSize childMainBefore = GetMainBefore(ref child.Element.Data, layoutType); + RelativeSize childMain = GetMain(ref child.Element.Data, layoutType); + RelativeSize childMainAfter = GetMainAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childMainBefore.IsAuto) @@ -1154,11 +1154,11 @@ private static void ProcessChildCrossSpacing( double parentCross, double borderCrossBefore, double borderCrossAfter, - UnitValue elementChildCrossBefore, - UnitValue elementChildCrossAfter) + RelativeSize elementChildCrossBefore, + RelativeSize elementChildCrossAfter) { - UnitValue childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - UnitValue childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); + RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); + RelativeSize childCrossAfter = GetCrossAfter(ref child.Element.Data, layoutType); // Apply parent overrides if (childCrossBefore.IsAuto) diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs new file mode 100644 index 0000000..7df7026 --- /dev/null +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -0,0 +1,469 @@ +using Prowl.PaperUI; + +namespace Prowl.PaperUI.LayoutEngine +{ + public readonly struct ScalingSettings + { + /// + /// The scaling factor applied to point units. + /// Eg: A value of 2 means that each point is equal to 2 pixels. + /// + public readonly float PointUnitScale = 1; + + public ScalingSettings() { } + } + + /// + /// Defines measurement units for element dimensions and positioning. + /// + public enum AbsoluteUnits + { + /// + /// 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. + /// + Percentage, + + /// + /// Flexible sizing that distributes available space based on stretch factors. + /// + Stretch, + + /// + /// Size is determined automatically based on content or other constraints. + /// + Auto + } + + public static class UnitValue + { + public static readonly AbsoluteSize ZeroPixels = new AbsoluteSize(AbsoluteUnits.Pixels, 0); + public static readonly RelativeSize Auto = new RelativeSize(RelativeUnits.Auto); + public static readonly RelativeSize StretchOne = new RelativeSize(RelativeUnits.Stretch, 1); + + /// + /// Creates a Pixel unit value. + /// + /// Size in pixels + public static AbsoluteSize Pixels(double value) => new AbsoluteSize(AbsoluteUnits.Pixels, value); + + /// + /// Creates a Points unit value. + /// + /// Size in points + public static AbsoluteSize Points(double value) => new AbsoluteSize(AbsoluteUnits.Points, value); + + /// + /// Creates a Stretch unit value with the specified factor. + /// + /// Stretch factor (relative to other stretch elements) + public static RelativeSize Stretch(double factor = 1f) => new RelativeSize(RelativeUnits.Stretch, factor); + + /// + /// Creates a Percentage unit value. + /// + /// Percentage value (0-100) + /// Additional pixel offset + public static RelativeSize Percentage(double value, double offset = 0f) => new RelativeSize(RelativeUnits.Percentage, value, offset); + } + + /// + /// Represents a value with a unit type for UI layout measurements. + /// Supports pixels and points. + /// + public struct AbsoluteSize : IEquatable + { + /// 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; + + /// + /// Creates a default AbsoluteSize with Points units. + /// + public AbsoluteSize() { } + + /// + /// Creates a AbsoluteSize with the specified type and value. + /// + /// The unit type + /// The numeric value + public AbsoluteSize(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) + { + return Type switch { + AbsoluteUnits.Pixels => Value, + AbsoluteUnits.Points => Value * scalingSettings.PointUnitScale, + _ => throw new ArgumentOutOfRangeException() + }; + } + + #region Implicit Conversions + + /// + /// Implicitly converts an integer to a point unit AbsoluteSize. + /// + public static implicit operator AbsoluteSize(int value) + { + return new AbsoluteSize(AbsoluteUnits.Points, value); + } + + /// + /// Implicitly converts a double to a point unit AbsoluteSize. + /// + public static implicit operator AbsoluteSize(double value) + { + return new AbsoluteSize(AbsoluteUnits.Points, value); + } + + /// + /// Implicitly converts an AbsoluteSize to a RelativeSize. + /// + public static implicit operator RelativeSize(AbsoluteSize value) + { + var relativeUnitType = value.Type switch + { + AbsoluteUnits.Pixels => RelativeUnits.Pixels, + AbsoluteUnits.Points => RelativeUnits.Points, + _ => throw new ArgumentOutOfRangeException() + }; + + return new RelativeSize(relativeUnitType, value.Value); + } + + #endregion + + #region Equality and Hashing + + public static bool operator ==(AbsoluteSize left, AbsoluteSize right) + { + return left.Equals(right); + } + + public static bool operator !=(AbsoluteSize left, AbsoluteSize right) + { + return !left.Equals(right); + } + + /// + /// Compares this AbsoluteSize with another object for equality. + /// + public override readonly bool Equals(object? obj) + { + return obj is AbsoluteSize other && Equals(other); + } + + public readonly bool Equals(AbsoluteSize other) + { + return Type == other.Type && Value.Equals(other.Value); + } + + /// + /// Returns a hash code for this AbsoluteSize. + /// + public override readonly int GetHashCode() + { + return HashCode.Combine((int)Type, Value); + } + + #endregion + + /// + /// Returns a string representation of this AbsoluteSize. + /// + public override readonly string ToString() => Type switch { + AbsoluteUnits.Pixels => $"{Value}px", + AbsoluteUnits.Points => $"{Value}pt", + _ => throw new ArgumentOutOfRangeException() + }; + } + + /// + /// 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 RelativeSize : IEquatable + { + /// + /// Helper class for interpolation between two RelativeSize instances. + /// Using a simplified class approach to avoid struct cycles. + /// + private class LerpData + { + public readonly RelativeSize Start; + public readonly RelativeSize End; + public readonly double Progress; + + public LerpData(RelativeSize start, RelativeSize 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 RelativeSizes (null when not interpolating) + private LerpData? _lerpData = null; + + /// + /// Creates a default RelativeSize with Auto units. + /// + public RelativeSize() { } + + /// + /// Creates a RelativeSize with the specified type and value. + /// + /// The unit type + /// The numeric value + /// Additional pixel offset for percentage units + public RelativeSize(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.PointUnitScale, + 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 RelativeSize min, in RelativeSize 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 RelativeSize instances. + /// In reality, it creates a new RelativeSize with special interpolation data which is calculated when ToPx is called. + /// + /// Starting value + /// Ending value + /// Interpolation factor (0.0 to 1.0) + /// Interpolated RelativeSize + public static RelativeSize Lerp(in RelativeSize a, in RelativeSize 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 RelativeSize( + 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 RelativeSize { + Type = a.Type, + Value = a.Value, + PercentPixelOffset = a.PercentPixelOffset, + _lerpData = new LerpData(a, b, blendFactor) + }; + return result; + } + + /// + /// Creates a deep copy of this RelativeSize. + /// + /// A new RelativeSize with the same properties + public readonly RelativeSize Clone() => new RelativeSize { + 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 RelativeSize. + /// + public static implicit operator RelativeSize(int value) + { + return new RelativeSize(RelativeUnits.Points, value); + } + + /// + /// Implicitly converts a double to a point unit RelativeSize. + /// + public static implicit operator RelativeSize(double value) + { + return new RelativeSize(RelativeUnits.Points, value); + } + + #endregion + + #region Equality and Hashing + + public static bool operator ==(RelativeSize left, RelativeSize right) + { + return left.Equals(right); + } + + public static bool operator !=(RelativeSize left, RelativeSize right) + { + return !left.Equals(right); + } + + /// + /// Compares this RelativeSize with another object for equality. + /// + public override readonly bool Equals(object? obj) + { + return obj is RelativeSize other && Equals(other); + } + + public readonly bool Equals(RelativeSize 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 RelativeSize. + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(_lerpData, (int)Type, Value, PercentPixelOffset); + } + + #endregion + + /// + /// Returns a string representation of this RelativeSize. + /// + 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/UnitValue.cs b/Paper/LayoutEngine/UnitValue.cs deleted file mode 100644 index 66f2f32..0000000 --- a/Paper/LayoutEngine/UnitValue.cs +++ /dev/null @@ -1,257 +0,0 @@ -using Prowl.PaperUI; - -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. - /// - public struct 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; - - /// - /// Creates a default UnitValue with Auto units. - /// - public UnitValue() { } - - /// - /// Creates a UnitValue with the specified type and 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); - - /// - /// 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); - - /// - /// 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() - }; - } -} diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 0c72af3..67e3eed 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -714,22 +714,22 @@ private void EndOfFrameCleanupStorage() /// /// Creates a stretch unit value with the specified factor. /// - public UnitValue Stretch(double factor = 1f) => UnitValue.Stretch(factor); + public RelativeSize Stretch(double factor = 1f) => UnitValue.Stretch(factor); /// /// Creates a pixel-based unit value. /// - public UnitValue Pixels(double value) => UnitValue.Pixels(value); + public RelativeSize Pixels(double value) => UnitValue.Pixels(value); /// /// Creates a percentage-based unit value with optional pixel offset. /// - public UnitValue Percent(double value, double pixelOffset = 0f) => UnitValue.Percentage(value, pixelOffset); + public RelativeSize Percent(double value, double pixelOffset = 0f) => UnitValue.Percentage(value, pixelOffset); /// /// Creates an auto-sized unit value. /// - public UnitValue Auto => UnitValue.Auto; + public RelativeSize Auto => UnitValue.Auto; #endregion } diff --git a/Paper/Paper.ElementStorage.cs b/Paper/Paper.ElementStorage.cs index 7369a8a..6f639cc 100644 --- a/Paper/Paper.ElementStorage.cs +++ b/Paper/Paper.ElementStorage.cs @@ -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.Styles.cs b/Paper/Paper.Styles.cs index d628ead..ff0b07a 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -597,9 +597,9 @@ 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 RelativeSize unitStart && end is RelativeSize unitEnd) { - return UnitValue.Lerp(unitStart, unitEnd, t); + return RelativeSize.Lerp(unitStart, unitEnd, t); } else if (start is Transform2D transformStart && end is Transform2D transformEnd) { From 6950b8ad3fddcec6fb4c10328aaa9ab28b26c8fd Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 09:55:53 -0400 Subject: [PATCH 02/38] Add Paper.SetPointUnitScale() --- Paper/LayoutEngine/RelativeSize.cs | 4 ++-- Paper/Paper.Core.cs | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 7df7026..2116c6f 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -2,13 +2,13 @@ namespace Prowl.PaperUI.LayoutEngine { - public readonly struct ScalingSettings + 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 readonly float PointUnitScale = 1; + public double PointUnitScale = 1; public ScalingSettings() { } } diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 67e3eed..5a693bd 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; private Stopwatch _timer = new(); // Events @@ -92,6 +93,14 @@ public void SetResolution(double width, double height) _height = height; } + /// + /// Sets the scaling factor applied to Points units. + /// + public void SetPointUnitScale(double pointUnitScale) + { + _scalingSettings.PointUnitScale = pointUnitScale; + } + public void AddFallbackFont(FontFile font) => _canvas.AddFallbackFont(font); public IEnumerable EnumerateSystemFonts() => _canvas.EnumerateSystemFonts(); From 5dcc305d08de42928b9bdbeb10e0a397ddff4fe5 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 10:57:06 -0400 Subject: [PATCH 03/38] Update implementation code to use ScalingSettings struct --- Paper/LayoutEngine/ElementLayout.cs | 128 ++++++++++++++-------------- Paper/Paper.Core.cs | 4 +- Paper/Paper.ElementStorage.cs | 4 +- Paper/Paper.Styles.cs | 32 +++---- 4 files changed, 86 insertions(+), 82 deletions(-) diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 6ee9223..2722005 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -9,7 +9,7 @@ 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; @@ -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); @@ -119,7 +119,7 @@ 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; @@ -129,34 +129,34 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay 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,13 +207,13 @@ 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(); @@ -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; } @@ -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); @@ -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,6 +914,7 @@ 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, @@ -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,6 +1035,7 @@ private static void ProcessChildCrossStretching( private static void ProcessChildMainStretching( ChildElementInfo child, LayoutType layoutType, + in ScalingSettings scalingSettings, double parentMain, double parentCross, double borderMainBefore, @@ -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,6 +1154,7 @@ private static void ProcessChildMainStretching( private static void ProcessChildCrossSpacing( ChildElementInfo child, LayoutType layoutType, + in ScalingSettings scalingSettings, double parentCross, double borderCrossBefore, double borderCrossAfter, @@ -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) diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 5a693bd..742708a 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -27,7 +27,7 @@ public partial class Paper private ICanvasRenderer _renderer; private double _width; private double _height; - private ScalingSettings _scalingSettings; + private ScalingSettings _scalingSettings = new ScalingSettings(); private Stopwatch _timer = new(); // Events @@ -150,7 +150,7 @@ public void EndFrame() // Layout phase OnEndOfFramePreLayout?.Invoke(); - ElementLayout.Layout(_rootElementHandle, this); + ElementLayout.Layout(_rootElementHandle, this, _scalingSettings); OnEndOfFramePostLayout?.Invoke(); // Post-layout callbacks diff --git a/Paper/Paper.ElementStorage.cs b/Paper/Paper.ElementStorage.cs index 6f639cc..6795759 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, (RelativeSize)UnitValue.Pixels(width)); + rootData._elementStyle.SetDirectValue(GuiProp.Height, (RelativeSize)UnitValue.Pixels(height)); } internal void ClearElements() diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index ff0b07a..c01cb51 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -674,24 +674,24 @@ public static void InitializeDefaults() _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.MinWidth] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxWidth] = (RelativeSize)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinHeight] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxHeight] = (RelativeSize)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.MinLeft] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxLeft] = (RelativeSize)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinRight] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxRight] = (RelativeSize)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinTop] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxTop] = (RelativeSize)UnitValue.Pixels(double.MaxValue); + _defaultValues[(int)GuiProp.MinBottom] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.MaxBottom] = (RelativeSize)UnitValue.Pixels(double.MaxValue); // Child Layout Properties _defaultValues[(int)GuiProp.ChildLeft] = UnitValue.Auto; @@ -700,10 +700,10 @@ public static void InitializeDefaults() _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.BorderLeft] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderRight] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderTop] = (RelativeSize)UnitValue.Pixels(0); + _defaultValues[(int)GuiProp.BorderBottom] = (RelativeSize)UnitValue.Pixels(0); // Transform Properties _defaultValues[(int)GuiProp.TranslateX] = 0.0; From d08f3eff398d200f47d98e463cf6bff6487bd03b Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 10:59:36 -0400 Subject: [PATCH 04/38] Explicitly specify size types --- Paper/Paper.Styles.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index c01cb51..ae06669 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -672,18 +672,18 @@ public static void InitializeDefaults() // Core Layout Properties _defaultValues[(int)GuiProp.AspectRatio] = -1.0; - _defaultValues[(int)GuiProp.Width] = UnitValue.Stretch(); - _defaultValues[(int)GuiProp.Height] = UnitValue.Stretch(); + _defaultValues[(int)GuiProp.Width] = (RelativeSize)UnitValue.Stretch(); + _defaultValues[(int)GuiProp.Height] = (RelativeSize)UnitValue.Stretch(); _defaultValues[(int)GuiProp.MinWidth] = (RelativeSize)UnitValue.Pixels(0); _defaultValues[(int)GuiProp.MaxWidth] = (RelativeSize)UnitValue.Pixels(double.MaxValue); _defaultValues[(int)GuiProp.MinHeight] = (RelativeSize)UnitValue.Pixels(0); _defaultValues[(int)GuiProp.MaxHeight] = (RelativeSize)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.Left] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.Right] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.Top] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.Bottom] = (RelativeSize)UnitValue.Auto; _defaultValues[(int)GuiProp.MinLeft] = (RelativeSize)UnitValue.Pixels(0); _defaultValues[(int)GuiProp.MaxLeft] = (RelativeSize)UnitValue.Pixels(double.MaxValue); _defaultValues[(int)GuiProp.MinRight] = (RelativeSize)UnitValue.Pixels(0); @@ -694,12 +694,12 @@ public static void InitializeDefaults() _defaultValues[(int)GuiProp.MaxBottom] = (RelativeSize)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.ChildLeft] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildRight] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildTop] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.ChildBottom] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.RowBetween] = (RelativeSize)UnitValue.Auto; + _defaultValues[(int)GuiProp.ColBetween] = (RelativeSize)UnitValue.Auto; _defaultValues[(int)GuiProp.BorderLeft] = (RelativeSize)UnitValue.Pixels(0); _defaultValues[(int)GuiProp.BorderRight] = (RelativeSize)UnitValue.Pixels(0); _defaultValues[(int)GuiProp.BorderTop] = (RelativeSize)UnitValue.Pixels(0); From 67208bfb0917a0bbb978304808bf7309c3f40214 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 11:13:52 -0400 Subject: [PATCH 05/38] Implement font scaling --- Paper/ElementBuilder.cs | 10 +++++----- Paper/LayoutEngine/ElementLayout.cs | 2 +- Paper/Paper.Core.cs | 3 ++- Paper/Paper.Styles.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 1825255..02828eb 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -245,7 +245,7 @@ public T Border(in RelativeSize left, in RelativeSize right, in RelativeSize top /// 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 AbsoluteSize size) => SetStyleProperty(GuiProp.FontSize, size); #endregion @@ -1346,7 +1346,7 @@ private void SaveTextInputState(TextInputState state) private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSettings, bool isMultiLine, double maxWidth = float.MaxValue) { - var fontSize = (double)_handle.Data._elementStyle.GetValue(GuiProp.FontSize); + var fontSize = ((AbsoluteSize)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); var settings = TextLayoutSettings.Default; @@ -1996,7 +1996,7 @@ private ElementBuilder CreateTextInput( canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); - var fontSize = (double)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize); + var fontSize = ((AbsoluteSize)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); // Draw text or placeholder if (string.IsNullOrEmpty(renderState.Value)) @@ -2153,8 +2153,8 @@ 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 = ((AbsoluteSize)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); + var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); double visibleWidth = _handle.Data.LayoutWidth; diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 2722005..e25744b 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -1309,7 +1309,7 @@ internal static Vector2 ProcessText(this ref ElementData element, Paper gui, flo settings.LetterSpacing = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.LetterSpacing)); 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)((AbsoluteSize)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/Paper.Core.cs b/Paper/Paper.Core.cs index 742708a..aa2da71 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -27,9 +27,10 @@ public partial class Paper private ICanvasRenderer _renderer; private double _width; private double _height; - private ScalingSettings _scalingSettings = new ScalingSettings(); private Stopwatch _timer = new(); + internal ScalingSettings _scalingSettings = new ScalingSettings(); + // Events public Action? OnEndOfFramePreLayout = null; public Action? OnEndOfFramePostLayout = null; diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index ae06669..27e6147 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -723,7 +723,7 @@ public static void InitializeDefaults() _defaultValues[(int)GuiProp.LetterSpacing] = 0.0; _defaultValues[(int)GuiProp.LineHeight] = 1.0; _defaultValues[(int)GuiProp.TabSize] = 4; - _defaultValues[(int)GuiProp.FontSize] = 16.0; + _defaultValues[(int)GuiProp.FontSize] = (AbsoluteSize)16.0; _initialized = true; } From d87333926bf7339b990f01216f54c012c87cecfc Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 11:24:18 -0400 Subject: [PATCH 06/38] Rename Absolute/RelativeSize to Absolute/RelativeUnit --- Paper/ElementBuilder.cs | 82 +++++++-------- Paper/LayoutEngine/ElementLayout.cs | 158 ++++++++++++++-------------- Paper/LayoutEngine/RelativeSize.cs | 122 ++++++++++----------- Paper/Paper.Core.cs | 8 +- Paper/Paper.ElementStorage.cs | 4 +- Paper/Paper.Styles.cs | 62 +++++------ 6 files changed, 218 insertions(+), 218 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 02828eb..50e8552 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -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 RelativeSize 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 RelativeSize width, in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize left, in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize horizontal, in RelativeSize 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 RelativeSize left, in RelativeSize right, in RelativeSize top, in RelativeSize 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 RelativeSize left, in RelativeSize right, in RelativeSize top } /// Sets the left padding for child elements. - public T ChildLeft(in RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize 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 RelativeSize colBetween) => SetStyleProperty(GuiProp.ColBetween, colBetween); + public T ColBetween(in RelativeUnit colBetween) => SetStyleProperty(GuiProp.ColBetween, colBetween); /// Sets the left border width. - public T BorderLeft(in RelativeSize borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); + public T BorderLeft(in RelativeUnit borderLeft) => SetStyleProperty(GuiProp.BorderLeft, borderLeft); /// Sets the right border width. - public T BorderRight(in RelativeSize borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); + public T BorderRight(in RelativeUnit borderRight) => SetStyleProperty(GuiProp.BorderRight, borderRight); /// Sets the top border width. - public T BorderTop(in RelativeSize borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); + public T BorderTop(in RelativeUnit borderTop) => SetStyleProperty(GuiProp.BorderTop, borderTop); /// Sets the bottom border width. - public T BorderBottom(in RelativeSize 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 RelativeSize 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 RelativeSize horizontal, in RelativeSize 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 RelativeSize left, in RelativeSize right, in RelativeSize top, in RelativeSize bottom) + public T Border(in RelativeUnit left, in RelativeUnit right, in RelativeUnit top, in RelativeUnit bottom) { SetStyleProperty(GuiProp.BorderLeft, left); SetStyleProperty(GuiProp.BorderRight, right); @@ -245,7 +245,7 @@ public T Border(in RelativeSize left, in RelativeSize right, in RelativeSize top /// 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(in AbsoluteSize size) => SetStyleProperty(GuiProp.FontSize, size); + public T FontSize(in AbsoluteUnit size) => SetStyleProperty(GuiProp.FontSize, size); #endregion @@ -1346,7 +1346,7 @@ private void SaveTextInputState(TextInputState state) private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSettings, bool isMultiLine, double maxWidth = float.MaxValue) { - var fontSize = ((AbsoluteSize)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); + var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); var settings = TextLayoutSettings.Default; @@ -1996,7 +1996,7 @@ private ElementBuilder CreateTextInput( canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); - var fontSize = ((AbsoluteSize)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); + var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); // Draw text or placeholder if (string.IsNullOrEmpty(renderState.Value)) @@ -2153,7 +2153,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set else { // Single-line horizontal scrolling only - var fontSize = ((AbsoluteSize)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); + var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index e25744b..4ac62e8 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -13,8 +13,8 @@ internal static UISize Layout(ElementHandle elementHandle, Paper gui, in Scaling { ref var data = ref elementHandle.Data; - var wValue = (RelativeSize)data._elementStyle.GetValue(GuiProp.Width); - var hValue = (RelativeSize)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"); @@ -31,23 +31,23 @@ internal static UISize Layout(ElementHandle elementHandle, Paper gui, in Scaling return size; } - private static RelativeSize GetProp(ref ElementData element, LayoutType parentType, GuiProp row, GuiProp column) - => (RelativeSize)element._elementStyle.GetValue(parentType == LayoutType.Row ? row : column); - - private static RelativeSize GetMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Width, GuiProp.Height); - private static RelativeSize GetCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Height, GuiProp.Width); - private static RelativeSize GetMinMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinWidth, GuiProp.MinHeight); - private static RelativeSize GetMaxMain(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxWidth, GuiProp.MaxHeight); - private static RelativeSize GetMinCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MinHeight, GuiProp.MinWidth); - private static RelativeSize GetMaxCross(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.MaxHeight, GuiProp.MaxWidth); - private static RelativeSize GetMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Left, GuiProp.Top); - private static RelativeSize GetMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Right, GuiProp.Bottom); - private static RelativeSize GetCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Top, GuiProp.Left); - private static RelativeSize GetCrossAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.Bottom, GuiProp.Right); - private static RelativeSize GetChildMainBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildLeft, GuiProp.ChildTop); - private static RelativeSize GetChildMainAfter(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildRight, GuiProp.ChildBottom); - private static RelativeSize GetChildCrossBefore(ref ElementData element, LayoutType parentType) => GetProp(ref element, parentType, GuiProp.ChildTop, GuiProp.ChildLeft); - private static RelativeSize 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 RelativeSize GetMainBetween(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.RowBetween, GuiProp.ColBetween); - private static RelativeSize GetMinMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinLeft, GuiProp.MinTop); - private static RelativeSize GetMaxMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxLeft, GuiProp.MaxTop); - private static RelativeSize GetMinMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinRight, GuiProp.MinBottom); - private static RelativeSize GetMaxMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxRight, GuiProp.MaxBottom); - private static RelativeSize GetMinCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinTop, GuiProp.MinLeft); - private static RelativeSize GetMaxCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxTop, GuiProp.MaxLeft); - private static RelativeSize GetMinCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MinBottom, GuiProp.MinRight); - private static RelativeSize GetMaxCrossAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.MaxBottom, GuiProp.MaxRight); - private static RelativeSize GetBorderMainBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderLeft, GuiProp.BorderTop); - private static RelativeSize GetBorderMainAfter(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderRight, GuiProp.BorderBottom); - private static RelativeSize GetBorderCrossBefore(ref ElementData element, LayoutType parentLayoutType) => GetProp(ref element, parentLayoutType, GuiProp.BorderTop, GuiProp.BorderLeft); - private static RelativeSize 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) { @@ -124,8 +124,8 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay ref var element = ref elementHandle.Data; LayoutType layoutType = element.LayoutType; - RelativeSize main = GetMain(ref element, parentLayoutType); - RelativeSize cross = GetCross(ref element, parentLayoutType); + RelativeUnit main = GetMain(ref element, parentLayoutType); + RelativeUnit cross = GetCross(ref element, parentLayoutType); double minMain = main.IsStretch ? DEFAULT_MIN @@ -284,11 +284,11 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay var mainAxis = new List(); // Parent overrides for child auto space - RelativeSize elementChildMainBefore = GetChildMainBefore(ref element, layoutType); - RelativeSize elementChildMainAfter = GetChildMainAfter(ref element, layoutType); - RelativeSize elementChildCrossBefore = GetChildCrossBefore(ref element, layoutType); - RelativeSize elementChildCrossAfter = GetChildCrossAfter(ref element, layoutType); - RelativeSize 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 - RelativeSize childMainBefore = GetMainBefore(ref child, layoutType); - RelativeSize childMain = GetMain(ref child, layoutType); - RelativeSize childMainAfter = GetMainAfter(ref child, layoutType); + RelativeUnit childMainBefore = GetMainBefore(ref child, layoutType); + RelativeUnit childMain = GetMain(ref child, layoutType); + RelativeUnit childMainAfter = GetMainAfter(ref child, layoutType); - RelativeSize childCrossBefore = GetCrossBefore(ref child, layoutType); - RelativeSize childCross = GetCross(ref child, layoutType); - RelativeSize 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 - RelativeSize childMinCrossBefore = GetMinCrossBefore(ref child, layoutType); - RelativeSize childMaxCrossBefore = GetMaxCrossBefore(ref child, layoutType); - RelativeSize childMinCrossAfter = GetMinCrossAfter(ref child, layoutType); - RelativeSize childMaxCrossAfter = GetMaxCrossAfter(ref child, layoutType); - RelativeSize childMinMainBefore = GetMinMainBefore(ref child, layoutType); - RelativeSize childMaxMainBefore = GetMaxMainBefore(ref child, layoutType); - RelativeSize childMinMainAfter = GetMinMainAfter(ref child, layoutType); - RelativeSize childMaxMainAfter = GetMaxMainAfter(ref child, layoutType); - RelativeSize childMinMain = GetMinMain(ref child, layoutType); - RelativeSize 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) @@ -481,9 +481,9 @@ private static UISize DoLayout(ElementHandle elementHandle, LayoutType parentLay GetCross(ref child.Element.Data, layoutType).IsAuto) continue; - RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - RelativeSize childCross = GetCross(ref child.Element.Data, layoutType); - RelativeSize 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; @@ -772,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; - RelativeSize childMainBefore = GetMainBefore(ref childHandle.Data, layoutType); - RelativeSize childMain = GetMain(ref childHandle.Data, layoutType); - RelativeSize childMainAfter = GetMainAfter(ref childHandle.Data, layoutType); - RelativeSize childCrossBefore = GetCrossBefore(ref childHandle.Data, layoutType); - RelativeSize childCross = GetCross(ref childHandle.Data, layoutType); - RelativeSize 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) @@ -919,13 +919,13 @@ private static void ProcessChildCrossStretching( double parentMain, double borderCrossBefore, double borderCrossAfter, - RelativeSize elementChildCrossBefore, - RelativeSize elementChildCrossAfter, + RelativeUnit elementChildCrossBefore, + RelativeUnit elementChildCrossAfter, int childIndex) { - RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - RelativeSize childCross = GetCross(ref child.Element.Data, layoutType); - RelativeSize 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) @@ -1040,13 +1040,13 @@ private static void ProcessChildMainStretching( double parentCross, double borderMainBefore, double borderMainAfter, - RelativeSize elementChildMainBefore, - RelativeSize elementChildMainAfter, + RelativeUnit elementChildMainBefore, + RelativeUnit elementChildMainAfter, int childIndex) { - RelativeSize childMainBefore = GetMainBefore(ref child.Element.Data, layoutType); - RelativeSize childMain = GetMain(ref child.Element.Data, layoutType); - RelativeSize 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) @@ -1158,11 +1158,11 @@ private static void ProcessChildCrossSpacing( double parentCross, double borderCrossBefore, double borderCrossAfter, - RelativeSize elementChildCrossBefore, - RelativeSize elementChildCrossAfter) + RelativeUnit elementChildCrossBefore, + RelativeUnit elementChildCrossAfter) { - RelativeSize childCrossBefore = GetCrossBefore(ref child.Element.Data, layoutType); - RelativeSize 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) @@ -1309,7 +1309,7 @@ internal static Vector2 ProcessText(this ref ElementData element, Paper gui, flo settings.LetterSpacing = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.LetterSpacing)); settings.LineHeight = Convert.ToSingle(element._elementStyle.GetValue(GuiProp.LineHeight)); settings.TabSize = (int)element._elementStyle.GetValue(GuiProp.TabSize); - settings.PixelSize = (float)((AbsoluteSize)element._elementStyle.GetValue(GuiProp.FontSize)).ToPx(gui._scalingSettings); + 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/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 2116c6f..96f21c4 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -60,41 +60,41 @@ public enum RelativeUnits public static class UnitValue { - public static readonly AbsoluteSize ZeroPixels = new AbsoluteSize(AbsoluteUnits.Pixels, 0); - public static readonly RelativeSize Auto = new RelativeSize(RelativeUnits.Auto); - public static readonly RelativeSize StretchOne = new RelativeSize(RelativeUnits.Stretch, 1); + 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 Pixel unit value. /// /// Size in pixels - public static AbsoluteSize Pixels(double value) => new AbsoluteSize(AbsoluteUnits.Pixels, value); + public static AbsoluteUnit Pixels(double value) => new AbsoluteUnit(AbsoluteUnits.Pixels, value); /// /// Creates a Points unit value. /// /// Size in points - public static AbsoluteSize Points(double value) => new AbsoluteSize(AbsoluteUnits.Points, value); + 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 RelativeSize Stretch(double factor = 1f) => new RelativeSize(RelativeUnits.Stretch, factor); + 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 RelativeSize Percentage(double value, double offset = 0f) => new RelativeSize(RelativeUnits.Percentage, value, offset); + public static RelativeUnit Percentage(double value, double offset = 0f) => new RelativeUnit(RelativeUnits.Percentage, value, offset); } /// /// Represents a value with a unit type for UI layout measurements. /// Supports pixels and points. /// - public struct AbsoluteSize : IEquatable + public struct AbsoluteUnit : IEquatable { /// The unit type of this value public AbsoluteUnits Type { get; set; } = AbsoluteUnits.Points; @@ -103,16 +103,16 @@ public struct AbsoluteSize : IEquatable public double Value { get; set; } = 0f; /// - /// Creates a default AbsoluteSize with Points units. + /// Creates a default AbsoluteUnit with Points units. /// - public AbsoluteSize() { } + public AbsoluteUnit() { } /// - /// Creates a AbsoluteSize with the specified type and value. + /// Creates a AbsoluteUnit with the specified type and value. /// /// The unit type /// The numeric value - public AbsoluteSize(AbsoluteUnits type, double value = 0f) + public AbsoluteUnit(AbsoluteUnits type, double value = 0f) { Type = type; Value = value; @@ -145,25 +145,25 @@ public readonly double ToPx(in ScalingSettings scalingSettings) #region Implicit Conversions /// - /// Implicitly converts an integer to a point unit AbsoluteSize. + /// Implicitly converts an integer to a point unit AbsoluteUnit. /// - public static implicit operator AbsoluteSize(int value) + public static implicit operator AbsoluteUnit(int value) { - return new AbsoluteSize(AbsoluteUnits.Points, value); + return new AbsoluteUnit(AbsoluteUnits.Points, value); } /// - /// Implicitly converts a double to a point unit AbsoluteSize. + /// Implicitly converts a double to a point unit AbsoluteUnit. /// - public static implicit operator AbsoluteSize(double value) + public static implicit operator AbsoluteUnit(double value) { - return new AbsoluteSize(AbsoluteUnits.Points, value); + return new AbsoluteUnit(AbsoluteUnits.Points, value); } /// - /// Implicitly converts an AbsoluteSize to a RelativeSize. + /// Implicitly converts an AbsoluteUnit to a RelativeUnit. /// - public static implicit operator RelativeSize(AbsoluteSize value) + public static implicit operator RelativeUnit(AbsoluteUnit value) { var relativeUnitType = value.Type switch { @@ -172,38 +172,38 @@ public static implicit operator RelativeSize(AbsoluteSize value) _ => throw new ArgumentOutOfRangeException() }; - return new RelativeSize(relativeUnitType, value.Value); + return new RelativeUnit(relativeUnitType, value.Value); } #endregion #region Equality and Hashing - public static bool operator ==(AbsoluteSize left, AbsoluteSize right) + public static bool operator ==(AbsoluteUnit left, AbsoluteUnit right) { return left.Equals(right); } - public static bool operator !=(AbsoluteSize left, AbsoluteSize right) + public static bool operator !=(AbsoluteUnit left, AbsoluteUnit right) { return !left.Equals(right); } /// - /// Compares this AbsoluteSize with another object for equality. + /// Compares this AbsoluteUnit with another object for equality. /// public override readonly bool Equals(object? obj) { - return obj is AbsoluteSize other && Equals(other); + return obj is AbsoluteUnit other && Equals(other); } - public readonly bool Equals(AbsoluteSize other) + public readonly bool Equals(AbsoluteUnit other) { return Type == other.Type && Value.Equals(other.Value); } /// - /// Returns a hash code for this AbsoluteSize. + /// Returns a hash code for this AbsoluteUnit. /// public override readonly int GetHashCode() { @@ -213,7 +213,7 @@ public override readonly int GetHashCode() #endregion /// - /// Returns a string representation of this AbsoluteSize. + /// Returns a string representation of this AbsoluteUnit. /// public override readonly string ToString() => Type switch { AbsoluteUnits.Pixels => $"{Value}px", @@ -226,19 +226,19 @@ public override readonly int GetHashCode() /// 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 RelativeSize : IEquatable + public struct RelativeUnit : IEquatable { /// - /// Helper class for interpolation between two RelativeSize instances. + /// Helper class for interpolation between two RelativeUnit instances. /// Using a simplified class approach to avoid struct cycles. /// private class LerpData { - public readonly RelativeSize Start; - public readonly RelativeSize End; + public readonly RelativeUnit Start; + public readonly RelativeUnit End; public readonly double Progress; - public LerpData(RelativeSize start, RelativeSize end, double progress) + public LerpData(RelativeUnit start, RelativeUnit end, double progress) { Start = start; End = end; @@ -255,21 +255,21 @@ public LerpData(RelativeSize start, RelativeSize end, double progress) /// Additional pixel offset when using percentage units public double PercentPixelOffset { get; set; } = default; - /// Data for interpolation between two RelativeSizes (null when not interpolating) + /// Data for interpolation between two RelativeUnits (null when not interpolating) private LerpData? _lerpData = null; /// - /// Creates a default RelativeSize with Auto units. + /// Creates a default RelativeUnit with Auto units. /// - public RelativeSize() { } + public RelativeUnit() { } /// - /// Creates a RelativeSize with the specified type and value. + /// Creates a RelativeUnit with the specified type and value. /// /// The unit type /// The numeric value /// Additional pixel offset for percentage units - public RelativeSize(RelativeUnits type, double value = 0f, double offset = 0f) + public RelativeUnit(RelativeUnits type, double value = 0f, double offset = 0f) { Type = type; Value = value; @@ -330,7 +330,7 @@ public readonly double ToPx(double parentValue, double defaultValue, in ScalingS /// 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 RelativeSize min, in RelativeSize max, in ScalingSettings scalingSettings) + 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); @@ -340,14 +340,14 @@ public readonly double ToPxClamped(double parentValue, double defaultValue, in R } /// - /// Linearly interpolates between two RelativeSize instances. - /// In reality, it creates a new RelativeSize with special interpolation data which is calculated when ToPx is called. + /// 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 RelativeSize - public static RelativeSize Lerp(in RelativeSize a, in RelativeSize b, double blendFactor) + /// 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); @@ -355,7 +355,7 @@ public static RelativeSize Lerp(in RelativeSize a, in RelativeSize b, double ble // If units are the same, we can blend directly if (a.Type == b.Type) { - return new RelativeSize( + return new RelativeUnit( a.Type, a.Value + (b.Value - a.Value) * blendFactor, a.PercentPixelOffset + (b.PercentPixelOffset - a.PercentPixelOffset) * blendFactor @@ -363,7 +363,7 @@ public static RelativeSize Lerp(in RelativeSize a, in RelativeSize b, double ble } // If units are different, use interpolation data - var result = new RelativeSize { + var result = new RelativeUnit { Type = a.Type, Value = a.Value, PercentPixelOffset = a.PercentPixelOffset, @@ -373,10 +373,10 @@ public static RelativeSize Lerp(in RelativeSize a, in RelativeSize b, double ble } /// - /// Creates a deep copy of this RelativeSize. + /// Creates a deep copy of this RelativeUnit. /// - /// A new RelativeSize with the same properties - public readonly RelativeSize Clone() => new RelativeSize { + /// A new RelativeUnit with the same properties + public readonly RelativeUnit Clone() => new RelativeUnit { Type = Type, Value = Value, PercentPixelOffset = PercentPixelOffset, @@ -386,44 +386,44 @@ public static RelativeSize Lerp(in RelativeSize a, in RelativeSize b, double ble #region Implicit Conversions /// - /// Implicitly converts an integer to a point unit RelativeSize. + /// Implicitly converts an integer to a point unit RelativeUnit. /// - public static implicit operator RelativeSize(int value) + public static implicit operator RelativeUnit(int value) { - return new RelativeSize(RelativeUnits.Points, value); + return new RelativeUnit(RelativeUnits.Points, value); } /// - /// Implicitly converts a double to a point unit RelativeSize. + /// Implicitly converts a double to a point unit RelativeUnit. /// - public static implicit operator RelativeSize(double value) + public static implicit operator RelativeUnit(double value) { - return new RelativeSize(RelativeUnits.Points, value); + return new RelativeUnit(RelativeUnits.Points, value); } #endregion #region Equality and Hashing - public static bool operator ==(RelativeSize left, RelativeSize right) + public static bool operator ==(RelativeUnit left, RelativeUnit right) { return left.Equals(right); } - public static bool operator !=(RelativeSize left, RelativeSize right) + public static bool operator !=(RelativeUnit left, RelativeUnit right) { return !left.Equals(right); } /// - /// Compares this RelativeSize with another object for equality. + /// Compares this RelativeUnit with another object for equality. /// public override readonly bool Equals(object? obj) { - return obj is RelativeSize other && Equals(other); + return obj is RelativeUnit other && Equals(other); } - public readonly bool Equals(RelativeSize other) + public readonly bool Equals(RelativeUnit other) { // First, check the basic properties bool basicPropertiesEqual = Type == other.Type && @@ -445,7 +445,7 @@ public readonly bool Equals(RelativeSize other) } /// - /// Returns a hash code for this RelativeSize. + /// Returns a hash code for this RelativeUnit. /// public override readonly int GetHashCode() { @@ -455,7 +455,7 @@ public override readonly int GetHashCode() #endregion /// - /// Returns a string representation of this RelativeSize. + /// Returns a string representation of this RelativeUnit. /// public override readonly string ToString() => Type switch { RelativeUnits.Pixels => $"{Value}px", diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index aa2da71..1629430 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -724,22 +724,22 @@ private void EndOfFrameCleanupStorage() /// /// Creates a stretch unit value with the specified factor. /// - public RelativeSize Stretch(double factor = 1f) => UnitValue.Stretch(factor); + public RelativeUnit Stretch(double factor = 1f) => UnitValue.Stretch(factor); /// /// Creates a pixel-based unit value. /// - public RelativeSize Pixels(double value) => UnitValue.Pixels(value); + public RelativeUnit Pixels(double value) => UnitValue.Pixels(value); /// /// Creates a percentage-based unit value with optional pixel offset. /// - public RelativeSize 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 RelativeSize Auto => UnitValue.Auto; + public RelativeUnit Auto => UnitValue.Auto; #endregion } diff --git a/Paper/Paper.ElementStorage.cs b/Paper/Paper.ElementStorage.cs index 6795759..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, (RelativeSize)UnitValue.Pixels(width)); - rootData._elementStyle.SetDirectValue(GuiProp.Height, (RelativeSize)UnitValue.Pixels(height)); + rootData._elementStyle.SetDirectValue(GuiProp.Width, (RelativeUnit)UnitValue.Pixels(width)); + rootData._elementStyle.SetDirectValue(GuiProp.Height, (RelativeUnit)UnitValue.Pixels(height)); } internal void ClearElements() diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index 27e6147..9144e54 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -597,9 +597,9 @@ private object Interpolate(object start, object end, double t) { return Vector4.Lerp(vector4Start, vector4End, t); } - else if (start is RelativeSize unitStart && end is RelativeSize unitEnd) + else if (start is RelativeUnit unitStart && end is RelativeUnit unitEnd) { - return RelativeSize.Lerp(unitStart, unitEnd, t); + return RelativeUnit.Lerp(unitStart, unitEnd, t); } else if (start is Transform2D transformStart && end is Transform2D transformEnd) { @@ -672,38 +672,38 @@ public static void InitializeDefaults() // Core Layout Properties _defaultValues[(int)GuiProp.AspectRatio] = -1.0; - _defaultValues[(int)GuiProp.Width] = (RelativeSize)UnitValue.Stretch(); - _defaultValues[(int)GuiProp.Height] = (RelativeSize)UnitValue.Stretch(); - _defaultValues[(int)GuiProp.MinWidth] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxWidth] = (RelativeSize)UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinHeight] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxHeight] = (RelativeSize)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] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.Right] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.Top] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.Bottom] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.MinLeft] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxLeft] = (RelativeSize)UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinRight] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxRight] = (RelativeSize)UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinTop] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxTop] = (RelativeSize)UnitValue.Pixels(double.MaxValue); - _defaultValues[(int)GuiProp.MinBottom] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.MaxBottom] = (RelativeSize)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] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildRight] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildTop] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.ChildBottom] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.RowBetween] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.ColBetween] = (RelativeSize)UnitValue.Auto; - _defaultValues[(int)GuiProp.BorderLeft] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderRight] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderTop] = (RelativeSize)UnitValue.Pixels(0); - _defaultValues[(int)GuiProp.BorderBottom] = (RelativeSize)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; @@ -723,7 +723,7 @@ public static void InitializeDefaults() _defaultValues[(int)GuiProp.LetterSpacing] = 0.0; _defaultValues[(int)GuiProp.LineHeight] = 1.0; _defaultValues[(int)GuiProp.TabSize] = 4; - _defaultValues[(int)GuiProp.FontSize] = (AbsoluteSize)16.0; + _defaultValues[(int)GuiProp.FontSize] = (AbsoluteUnit)16.0; _initialized = true; } From 215bb4d6670801bf16f4290eba98e4c4d1eb909c Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 11:54:37 -0400 Subject: [PATCH 07/38] Explicitly use UnitValue.Points --- Paper/Paper.Styles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index 9144e54..03a8be8 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -723,7 +723,7 @@ public static void InitializeDefaults() _defaultValues[(int)GuiProp.LetterSpacing] = 0.0; _defaultValues[(int)GuiProp.LineHeight] = 1.0; _defaultValues[(int)GuiProp.TabSize] = 4; - _defaultValues[(int)GuiProp.FontSize] = (AbsoluteUnit)16.0; + _defaultValues[(int)GuiProp.FontSize] = (AbsoluteUnit)UnitValue.Points(16); _initialized = true; } From 38567ccd0e9beef5b56337f56ed63d0626deab2b Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 12:03:54 -0400 Subject: [PATCH 08/38] Apply scaling to word and letter spacing --- Paper/ElementBuilder.cs | 8 ++++---- Paper/LayoutEngine/ElementLayout.cs | 4 ++-- Paper/Paper.Styles.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 50e8552..d4801a2 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -236,9 +236,9 @@ public T Border(in RelativeUnit left, in RelativeUnit right, in RelativeUnit top 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); @@ -1347,7 +1347,7 @@ private void SaveTextInputState(TextInputState state) private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSettings, bool isMultiLine, double maxWidth = float.MaxValue) { var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); - var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); + var letterSpacing = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing)).ToPx(_paper._scalingSettings); var settings = TextLayoutSettings.Default; settings.PixelSize = (float)fontSize; @@ -2154,7 +2154,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set { // Single-line horizontal scrolling only var fontSize = ((AbsoluteUnit)_handle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); - var letterSpacing = (double)_handle.Data._elementStyle.GetValue(GuiProp.LetterSpacing); + 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; diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 4ac62e8..1e401af 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -1305,8 +1305,8 @@ 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 = (float)((AbsoluteUnit)element._elementStyle.GetValue(GuiProp.FontSize)).ToPx(gui._scalingSettings); diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index 03a8be8..03335d0 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -719,8 +719,8 @@ 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] = (AbsoluteUnit)UnitValue.Points(16); From e608b76d9fde8e3e1c3ebe80786badfa65abafed Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 12:15:04 -0400 Subject: [PATCH 09/38] Apply scaling to TranslateX/Y --- Paper/ElementBuilder.cs | 8 ++++---- Paper/Paper.Core.cs | 4 ++-- Paper/Paper.Interaction.cs | 28 ++++++++++++++-------------- Paper/Paper.Styles.cs | 12 ++++++------ 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index d4801a2..2087a85 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -52,7 +52,7 @@ 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(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) => @@ -252,13 +252,13 @@ public T Border(in RelativeUnit left, in RelativeUnit right, in RelativeUnit top #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); diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 1629430..88dbc58 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -251,7 +251,7 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List 0.0f && borderColor.A > 0) { _canvas.BeginPath(); 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 03335d0..054b533 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, 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); @@ -666,7 +666,7 @@ 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.BorderWidth] = (AbsoluteUnit)UnitValue.Points(0); _defaultValues[(int)GuiProp.Rounded] = new Vector4(0, 0, 0, 0); _defaultValues[(int)GuiProp.BoxShadow] = BoxShadow.None; @@ -706,8 +706,8 @@ public static void InitializeDefaults() _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; From 15cd6dada0d6a96750ceb0bc62687cafd4caa9e6 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 12:40:37 -0400 Subject: [PATCH 10/38] Apply scaling to rounding --- Paper/ElementBuilder.cs | 14 +++++++------- Paper/LayoutEngine/RelativeSize.cs | 24 ++++++++++++++++++++++++ Paper/Paper.Core.cs | 2 +- Paper/Paper.Styles.cs | 2 +- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 2087a85..16ac04a 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -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 diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 96f21c4..d02bf0a 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -1,7 +1,31 @@ using Prowl.PaperUI; +using Prowl.Vector; namespace Prowl.PaperUI.LayoutEngine { + public struct Rounding + { + public AbsoluteUnit TopLeft; + public AbsoluteUnit TopRight; + public AbsoluteUnit BottomRight; + public AbsoluteUnit BottomLeft; + + public Rounding(AbsoluteUnit topLeft, AbsoluteUnit topRight, AbsoluteUnit bottomRight, AbsoluteUnit bottomLeft) + { + TopLeft = topLeft; + TopRight = topRight; + BottomRight = bottomRight; + BottomLeft = bottomLeft; + } + + public Vector4 ToPx(ScalingSettings scalingSettings) + { + return new Vector4( + TopLeft.ToPx(scalingSettings), TopRight.ToPx(scalingSettings), + BottomRight.ToPx(scalingSettings), BottomLeft.ToPx(scalingSettings)); + } + } + public struct ScalingSettings { /// diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 88dbc58..b23a028 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -255,7 +255,7 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List Date: Wed, 17 Sep 2025 12:47:21 -0400 Subject: [PATCH 11/38] Add interpolation for absolute units --- Paper/LayoutEngine/RelativeSize.cs | 79 +++++++++++++++++++++++++++++- Paper/Paper.Styles.cs | 8 ++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index d02bf0a..8eb4f38 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -116,16 +116,37 @@ public static class UnitValue /// /// Represents a value with a unit type for UI layout measurements. - /// Supports pixels and points. + /// 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. /// @@ -159,6 +180,14 @@ public AbsoluteUnit(AbsoluteUnits type, double value = 0f) /// 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.PointUnitScale, @@ -166,6 +195,37 @@ public readonly double ToPx(in ScalingSettings scalingSettings) }; } + /// + /// 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 /// @@ -223,7 +283,22 @@ public override readonly bool Equals(object? obj) public readonly bool Equals(AbsoluteUnit other) { - return Type == other.Type && Value.Equals(other.Value); + // 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; } /// diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index b7227d3..8f3ec6f 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -597,9 +597,13 @@ private object Interpolate(object start, object end, double t) { return Vector4.Lerp(vector4Start, vector4End, t); } - else if (start is RelativeUnit unitStart && end is RelativeUnit unitEnd) + else if (start is RelativeUnit relativeUnitStart && end is RelativeUnit relativeUnitEnd) { - return RelativeUnit.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) { From 5c185e3b77333c9ed9af63299138c0c46ee3b835 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 12:51:57 -0400 Subject: [PATCH 12/38] Implement lerping for Rounding struct --- Paper/LayoutEngine/RelativeSize.cs | 16 ++++++++++++++++ Paper/Paper.Styles.cs | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 8eb4f38..253e55d 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -24,6 +24,22 @@ public Vector4 ToPx(ScalingSettings scalingSettings) 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)); + } } public struct ScalingSettings diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index 8f3ec6f..3765057 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -621,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; From fbad8adf57eb637a83db7507db3350ea8d8b86f1 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 12:59:38 -0400 Subject: [PATCH 13/38] Use in keyword consistently with AbsoluteUnit --- Paper/ElementBuilder.cs | 2 +- Paper/LayoutEngine/RelativeSize.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 16ac04a..39a9aaa 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -52,7 +52,7 @@ 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(AbsoluteUnit 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) => diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 253e55d..0b1c712 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -10,7 +10,7 @@ public struct Rounding public AbsoluteUnit BottomRight; public AbsoluteUnit BottomLeft; - public Rounding(AbsoluteUnit topLeft, AbsoluteUnit topRight, AbsoluteUnit bottomRight, AbsoluteUnit bottomLeft) + public Rounding(in AbsoluteUnit topLeft, in AbsoluteUnit topRight, in AbsoluteUnit bottomRight, in AbsoluteUnit bottomLeft) { TopLeft = topLeft; TopRight = topRight; From 72a0bb97e4b3015306edd9fcd51b36421a0d7df3 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:02:02 -0400 Subject: [PATCH 14/38] Apply scaling to box shadow --- Paper/BoxShadow.cs | 20 +++++++++++--------- Paper/ElementBuilder.cs | 2 +- Paper/Paper.Core.cs | 27 ++++++++++++++++----------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Paper/BoxShadow.cs b/Paper/BoxShadow.cs index d0bb1ef..5fa644b 100644 --- a/Paper/BoxShadow.cs +++ b/Paper/BoxShadow.cs @@ -1,5 +1,7 @@ using System.Drawing; +using Prowl.PaperUI.LayoutEngine; + namespace Prowl.PaperUI { /// @@ -7,13 +9,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 +45,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 39a9aaa..5596e01 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -55,7 +55,7 @@ public T BackgroundBoxGradient(double centerX, double centerY, double width, dou 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. diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index b23a028..e1a0eca 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -259,11 +259,16 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List Date: Wed, 17 Sep 2025 13:07:25 -0400 Subject: [PATCH 15/38] Fix incorrect toggle scaling in PaperDemo --- Samples/Shared/PaperDemo.Components.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Samples/Shared/PaperDemo.Components.cs b/Samples/Shared/PaperDemo.Components.cs index b24d9e0..2054f4e 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; } From 553983588b1ed10602183b406122b7ef42e97689 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:07:46 -0400 Subject: [PATCH 16/38] Add Paper.Points helper function --- Paper/Paper.Core.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index e1a0eca..6b95284 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -727,14 +727,19 @@ private void EndOfFrameCleanupStorage() #region Layout Helpers /// - /// Creates a stretch unit value with the specified factor. + /// Creates a pixel-based unit value. /// - public RelativeUnit 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 RelativeUnit Pixels(double value) => UnitValue.Pixels(value); + public AbsoluteUnit Points(double value) => UnitValue.Points(value); + + /// + /// Creates a stretch unit value with the specified factor. + /// + public RelativeUnit Stretch(double factor = 1f) => UnitValue.Stretch(factor); /// /// Creates a percentage-based unit value with optional pixel offset. From d0318939dbd6a355f18c0ccdde266a0bb071c3d5 Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:19:04 -0400 Subject: [PATCH 17/38] Rename PointUnitScale to ContentScale --- Paper/LayoutEngine/RelativeSize.cs | 6 +++--- Paper/Paper.Core.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Paper/LayoutEngine/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs index 0b1c712..c988655 100644 --- a/Paper/LayoutEngine/RelativeSize.cs +++ b/Paper/LayoutEngine/RelativeSize.cs @@ -48,7 +48,7 @@ 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 PointUnitScale = 1; + public double ContentScale = 1; public ScalingSettings() { } } @@ -206,7 +206,7 @@ public readonly double ToPx(in ScalingSettings scalingSettings) return Type switch { AbsoluteUnits.Pixels => Value, - AbsoluteUnits.Points => Value * scalingSettings.PointUnitScale, + AbsoluteUnits.Points => Value * scalingSettings.ContentScale, _ => throw new ArgumentOutOfRangeException() }; } @@ -430,7 +430,7 @@ public readonly double ToPx(double parentValue, double defaultValue, in ScalingS // Convert based on unit type return Type switch { RelativeUnits.Pixels => Value, - RelativeUnits.Points => Value * scalingSettings.PointUnitScale, + RelativeUnits.Points => Value * scalingSettings.ContentScale, RelativeUnits.Percentage => ((Value / 100f) * parentValue) + PercentPixelOffset, _ => defaultValue }; diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 6b95284..5bad219 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -97,9 +97,9 @@ public void SetResolution(double width, double height) /// /// Sets the scaling factor applied to Points units. /// - public void SetPointUnitScale(double pointUnitScale) + public void SetContentScale(double pointUnitScale) { - _scalingSettings.PointUnitScale = pointUnitScale; + _scalingSettings.ContentScale = pointUnitScale; } public void AddFallbackFont(FontFile font) => _canvas.AddFallbackFont(font); From c9596adec8fdca27d9ab3dcb76b4f029514b3f1a Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:19:14 -0400 Subject: [PATCH 18/38] Scale scroll speed by content scale --- Paper/ElementBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 5596e01..144e9d3 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1107,13 +1107,13 @@ private void ConfigureScrollHandlers() { state.Position = new Vector2( state.Position.x, - state.Position.y - e.Delta * 30 // Adjust scroll speed as needed + state.Position.y - e.Delta * 30 * _paper._scalingSettings.ContentScale // Adjust scroll speed as needed ); } else if ((_handle.Data.ScrollFlags & Scroll.ScrollX) != 0) { state.Position = new Vector2( - state.Position.x - e.Delta * 30, + state.Position.x - e.Delta * 30 * _paper._scalingSettings.ContentScale, state.Position.y ); } From a01cc3d9b6c7e698faf6ce70b54f7c334be521af Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:28:12 -0400 Subject: [PATCH 19/38] Scale a few hardcoded pixel values by content scale Not exhaustive, but these are the main ones that matter. --- Paper/ElementBuilder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 144e9d3..a5b5018 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1913,8 +1913,8 @@ private ElementBuilder CreateTextInput( if (currentState.SelectionStart < 0) return; // Auto-scroll when dragging near edges - const double edgeScrollSensitivity = 20.0; - const double scrollSpeed = 2.0; + double edgeScrollSensitivity = 20.0 * _paper._scalingSettings.ContentScale; + double scrollSpeed = 2.0 * _paper._scalingSettings.ContentScale; if (e.RelativePosition.x < edgeScrollSensitivity) currentState.ScrollOffsetX = Math.Max(0, currentState.ScrollOffsetX - scrollSpeed); @@ -2133,7 +2133,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set double visibleWidth = _handle.Data.LayoutWidth; double visibleHeight = _handle.Data.LayoutHeight; - const double margin = 10.0; + double margin = 10.0 * _paper._scalingSettings.ContentScale; // Horizontal scrolling if (cursorPos.X < state.ScrollOffsetX + margin) @@ -2158,7 +2158,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); double visibleWidth = _handle.Data.LayoutWidth; - const double margin = 20.0; + double margin = 20.0 * _paper._scalingSettings.ContentScale; if (cursorPos.x < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.x - margin); From dfb0e6e3d2dfa3097cc355e175653848e3e1ecfb Mon Sep 17 00:00:00 2001 From: William Chen Date: Wed, 17 Sep 2025 13:43:10 -0400 Subject: [PATCH 20/38] Move new types into their correct locations in the codebase --- Paper/Enums.cs | 45 ++ Paper/LayoutEngine/AbsoluteUnit.cs | 212 ++++++++++ Paper/LayoutEngine/RelativeSize.cs | 584 -------------------------- Paper/LayoutEngine/RelativeUnit.cs | 250 +++++++++++ Paper/LayoutEngine/ScalingSettings.cs | 22 + Paper/LayoutEngine/UnitValue.cs | 40 ++ Paper/Rounding.cs | 50 +++ 7 files changed, 619 insertions(+), 584 deletions(-) create mode 100644 Paper/LayoutEngine/AbsoluteUnit.cs delete mode 100644 Paper/LayoutEngine/RelativeSize.cs create mode 100644 Paper/LayoutEngine/RelativeUnit.cs create mode 100644 Paper/LayoutEngine/ScalingSettings.cs create mode 100644 Paper/LayoutEngine/UnitValue.cs create mode 100644 Paper/Rounding.cs diff --git a/Paper/Enums.cs b/Paper/Enums.cs index 77e1931..2c9bbd4 100644 --- a/Paper/Enums.cs +++ b/Paper/Enums.cs @@ -34,6 +34,51 @@ public enum PositionType ParentDirected } + /// + /// Defines measurement units for element dimensions and positioning. + /// + public enum AbsoluteUnits + { + /// + /// 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. + /// + Percentage, + + /// + /// Flexible sizing that distributes available space based on stretch factors. + /// + Stretch, + + /// + /// Size is determined automatically based on content or other constraints. + /// + Auto + } + public enum TextAlignment { Left, 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/RelativeSize.cs b/Paper/LayoutEngine/RelativeSize.cs deleted file mode 100644 index c988655..0000000 --- a/Paper/LayoutEngine/RelativeSize.cs +++ /dev/null @@ -1,584 +0,0 @@ -using Prowl.PaperUI; -using Prowl.Vector; - -namespace Prowl.PaperUI.LayoutEngine -{ - 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(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)); - } - } - - 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() { } - } - - /// - /// Defines measurement units for element dimensions and positioning. - /// - public enum AbsoluteUnits - { - /// - /// 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. - /// - Percentage, - - /// - /// Flexible sizing that distributes available space based on stretch factors. - /// - Stretch, - - /// - /// Size is determined automatically based on content or other constraints. - /// - Auto - } - - public static class UnitValue - { - 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 Pixel unit value. - /// - /// Size in pixels - public static AbsoluteUnit Pixels(double value) => new AbsoluteUnit(AbsoluteUnits.Pixels, value); - - /// - /// Creates a Points unit value. - /// - /// 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 RelativeUnit Stretch(double factor = 1f) => new RelativeUnit(RelativeUnits.Stretch, factor); - - /// - /// Creates a Percentage unit value. - /// - /// Percentage value (0-100) - /// Additional pixel offset - public static RelativeUnit Percentage(double value, double offset = 0f) => new RelativeUnit(RelativeUnits.Percentage, value, offset); - } - - /// - /// 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() - }; - } - - /// - /// 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/RelativeUnit.cs b/Paper/LayoutEngine/RelativeUnit.cs new file mode 100644 index 0000000..1dda1fb --- /dev/null +++ b/Paper/LayoutEngine/RelativeUnit.cs @@ -0,0 +1,250 @@ +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..2291279 --- /dev/null +++ b/Paper/LayoutEngine/ScalingSettings.cs @@ -0,0 +1,22 @@ +// 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() { } + } +} diff --git a/Paper/LayoutEngine/UnitValue.cs b/Paper/LayoutEngine/UnitValue.cs new file mode 100644 index 0000000..47ff48c --- /dev/null +++ b/Paper/LayoutEngine/UnitValue.cs @@ -0,0 +1,40 @@ +// 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 +{ + /// + /// Contains constants and helper methods for creating AbsoluteUnits and RelativeUnits. + /// + public static class UnitValue + { + 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 Pixel unit value. + /// + /// Size in pixels + public static AbsoluteUnit Pixels(double value) => new AbsoluteUnit(AbsoluteUnits.Pixels, value); + + /// + /// Creates a Points unit value. + /// + /// 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 RelativeUnit Stretch(double factor = 1f) => new RelativeUnit(RelativeUnits.Stretch, factor); + + /// + /// Creates a Percentage unit value. + /// + /// Percentage value (0-100) + /// Additional pixel offset + public static RelativeUnit Percentage(double value, double offset = 0f) => new RelativeUnit(RelativeUnits.Percentage, value, offset); + } +} diff --git a/Paper/Rounding.cs b/Paper/Rounding.cs new file mode 100644 index 0000000..1c0ce0f --- /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(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)); + } + } +} From baa5aae1d60e675cb4b60b8567c77e830314ba5e Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 04:38:25 -0400 Subject: [PATCH 21/38] Add Prowl license header to RelativeUnit --- Paper/LayoutEngine/RelativeUnit.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Paper/LayoutEngine/RelativeUnit.cs b/Paper/LayoutEngine/RelativeUnit.cs index 1dda1fb..dcb9c58 100644 --- a/Paper/LayoutEngine/RelativeUnit.cs +++ b/Paper/LayoutEngine/RelativeUnit.cs @@ -1,4 +1,7 @@ -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. + +using Prowl.PaperUI; using Prowl.Vector; namespace Prowl.PaperUI.LayoutEngine From e598183a4e5915190cb4eec9b9aed894aca667a6 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 04:38:35 -0400 Subject: [PATCH 22/38] Cleanup --- Paper/BoxShadow.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Paper/BoxShadow.cs b/Paper/BoxShadow.cs index 5fa644b..731c867 100644 --- a/Paper/BoxShadow.cs +++ b/Paper/BoxShadow.cs @@ -1,5 +1,4 @@ using System.Drawing; - using Prowl.PaperUI.LayoutEngine; namespace Prowl.PaperUI From e6c596be375a7763ce4827e1127ca1269c4c740d Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 04:54:25 -0400 Subject: [PATCH 23/38] Apply scaling to scrollbars --- Paper/ElementBuilder.cs | 16 ++++++------- Paper/Paper.Core.cs | 32 ++++++++++++++++++++------ Paper/ScrollState.cs | 50 +++++++++++++++++++++++------------------ 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index a5b5018..b02fd56 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1081,8 +1081,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); }); @@ -1131,8 +1131,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 +1162,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 +1183,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); }); diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 5bad219..73ba917 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -508,35 +508,53 @@ private void DrawDefaultScrollbars(Canvas canvas, Rect rect, ScrollState state, bool hasHorizontal = state.ContentSize.x > 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); } } diff --git a/Paper/ScrollState.cs b/Paper/ScrollState.cs index b9e82b3..d8ef1bf 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,10 @@ 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; + // These should be scaled by content scale before use + public static AbsoluteUnit ScrollbarSize = 12; + public static AbsoluteUnit ScrollbarMinSize = 20; + public static AbsoluteUnit ScrollbarPadding = 2; /// /// Gets the maximum scroll position. @@ -67,64 +69,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, 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, 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, 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 +141,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, 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 +157,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, 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 +182,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, 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; From 3095fc1ead64257320d1ece88416aa60e9dcc87d Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 04:57:27 -0400 Subject: [PATCH 24/38] Use UnitValue.Points and ToPx instead of scaling pixel values manually --- Paper/ElementBuilder.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index b02fd56..d1ae839 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1103,17 +1103,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 * _paper._scalingSettings.ContentScale // 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 * _paper._scalingSettings.ContentScale, + state.Position.x - e.Delta * scrollSpeed, state.Position.y ); } @@ -1913,8 +1916,8 @@ private ElementBuilder CreateTextInput( if (currentState.SelectionStart < 0) return; // Auto-scroll when dragging near edges - double edgeScrollSensitivity = 20.0 * _paper._scalingSettings.ContentScale; - double scrollSpeed = 2.0 * _paper._scalingSettings.ContentScale; + 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); @@ -2133,7 +2136,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set double visibleWidth = _handle.Data.LayoutWidth; double visibleHeight = _handle.Data.LayoutHeight; - double margin = 10.0 * _paper._scalingSettings.ContentScale; + double margin = UnitValue.Points(10).ToPx(_paper._scalingSettings); // Horizontal scrolling if (cursorPos.X < state.ScrollOffsetX + margin) @@ -2158,7 +2161,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set var cursorPos = GetCursorPositionFromIndex(state.Value, settings.Font, fontSize, letterSpacing, state.CursorPosition); double visibleWidth = _handle.Data.LayoutWidth; - double margin = 20.0 * _paper._scalingSettings.ContentScale; + double margin = UnitValue.Points(20).ToPx(_paper._scalingSettings); if (cursorPos.x < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.x - margin); From 2656f18a5cccf3da613afc4c724b68870c11533f Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 05:02:14 -0400 Subject: [PATCH 25/38] Expose scaling settings as a public property to allow users to call AbsoluteUnit.ToPx when necessary --- Paper/ElementBuilder.cs | 36 ++++++++++++++--------------- Paper/LayoutEngine/ElementLayout.cs | 6 ++--- Paper/Paper.Core.cs | 3 ++- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index d1ae839..4c2e38a 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1081,8 +1081,8 @@ private void ConfigureScrollHandlers() Vector2 mousePos = _paper.PointerPos; // Check if pointer is over scrollbars - state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); - state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); + 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); }); @@ -1104,7 +1104,7 @@ private void ConfigureScrollHandlers() return; // Adjust scroll speed as needed - double scrollSpeed = UnitValue.Points(30).ToPx(_paper._scalingSettings); + double scrollSpeed = UnitValue.Points(30).ToPx(_paper.ScalingSettings); if ((_handle.Data.ScrollFlags & Scroll.ScrollY) != 0) { @@ -1134,8 +1134,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, _paper._scalingSettings); - bool onHorizontal = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); + 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) @@ -1165,11 +1165,11 @@ private void ConfigureScrollHandlers() // Handle scrollbar dragging if (state.IsDraggingVertical) { - state.HandleVerticalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); + state.HandleVerticalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); } else if (state.IsDraggingHorizontal) { - state.HandleHorizontalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); + state.HandleHorizontalScrollbarDrag(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper.ScalingSettings); } _paper.SetElementStorage(_handle, "ScrollState", state); @@ -1186,8 +1186,8 @@ private void ConfigureScrollHandlers() // Update hover state on release Vector2 mousePos = _paper.PointerPos; - state.IsVerticalScrollbarHovered = state.IsPointOverVerticalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); - state.IsHorizontalScrollbarHovered = state.IsPointOverHorizontalScrollbar(mousePos, e.ElementRect, _handle.Data.ScrollFlags, _paper._scalingSettings); + 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); }); @@ -1349,8 +1349,8 @@ private void SaveTextInputState(TextInputState state) private TextLayoutSettings CreateTextLayoutSettings(TextInputSettings inputSettings, bool isMultiLine, double maxWidth = float.MaxValue) { - 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 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; @@ -1916,8 +1916,8 @@ private ElementBuilder CreateTextInput( if (currentState.SelectionStart < 0) return; // Auto-scroll when dragging near edges - double edgeScrollSensitivity = UnitValue.Points(20).ToPx(_paper._scalingSettings); - double scrollSpeed = UnitValue.Points(2).ToPx(_paper._scalingSettings); + 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); @@ -1999,7 +1999,7 @@ private ElementBuilder CreateTextInput( canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); - var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper._scalingSettings); + var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper.ScalingSettings); // Draw text or placeholder if (string.IsNullOrEmpty(renderState.Value)) @@ -2136,7 +2136,7 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set double visibleWidth = _handle.Data.LayoutWidth; double visibleHeight = _handle.Data.LayoutHeight; - double margin = UnitValue.Points(10).ToPx(_paper._scalingSettings); + double margin = UnitValue.Points(10).ToPx(_paper.ScalingSettings); // Horizontal scrolling if (cursorPos.X < state.ScrollOffsetX + margin) @@ -2156,12 +2156,12 @@ private void EnsureCursorVisible(ref TextInputState state, TextInputSettings set else { // Single-line horizontal scrolling only - 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 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; - double margin = UnitValue.Points(20).ToPx(_paper._scalingSettings); + double margin = UnitValue.Points(20).ToPx(_paper.ScalingSettings); if (cursorPos.x < state.ScrollOffsetX + margin) state.ScrollOffsetX = Math.Max(0, cursorPos.x - margin); diff --git a/Paper/LayoutEngine/ElementLayout.cs b/Paper/LayoutEngine/ElementLayout.cs index 1e401af..6035a36 100644 --- a/Paper/LayoutEngine/ElementLayout.cs +++ b/Paper/LayoutEngine/ElementLayout.cs @@ -1305,11 +1305,11 @@ internal static Vector2 ProcessText(this ref ElementData element, Paper gui, flo { var settings = TextLayoutSettings.Default; - 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.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 = (float)((AbsoluteUnit)element._elementStyle.GetValue(GuiProp.FontSize)).ToPx(gui._scalingSettings); + 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/Paper.Core.cs b/Paper/Paper.Core.cs index 73ba917..26c4c99 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -27,9 +27,9 @@ public partial class Paper private ICanvasRenderer _renderer; private double _width; private double _height; + private ScalingSettings _scalingSettings = new ScalingSettings(); private Stopwatch _timer = new(); - internal ScalingSettings _scalingSettings = new ScalingSettings(); // Events public Action? OnEndOfFramePreLayout = null; @@ -43,6 +43,7 @@ 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; /// /// Gets the current parent element in the element hierarchy. From a599e33078f1d3e5079e925dfe190ee5e564d82c Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 05:05:04 -0400 Subject: [PATCH 26/38] Add content scaling to README features list --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f87fe73..ee1d541 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,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 + - Pixel, 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 @@ -137,7 +138,7 @@ void RenderUI() { // Begin the UI frame Paper.BeginFrame(deltaTime); - + // Define your UI using (Paper.Column("MainContainer") .BackgroundColor(240, 240, 240) @@ -149,7 +150,7 @@ void RenderUI() .BackgroundColor(50, 120, 200) .Text(Text.Center("My Application", myFont, Color.White)) .Enter()) { } - + // Content area using (Paper.Row("Content").Enter()) { @@ -157,12 +158,12 @@ void RenderUI() Paper.Box("Sidebar") .Width(200) .BackgroundColor(220, 220, 220); - + // Main content Paper.Box("MainContent"); } } - + // End the UI frame Paper.EndFrame(); } @@ -264,7 +265,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 @@ -273,19 +274,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) @@ -293,7 +294,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) From 928471a7bd39e6c10f3372bc16163c9f67f3aff3 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 05:05:17 -0400 Subject: [PATCH 27/38] Fix typo in README: "any where" -> "anywhere --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee1d541..26eadc5 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,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)

From 04291bcdf05d54781d4cd2f0ddb68549e3d603dc Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 11:13:01 -0400 Subject: [PATCH 28/38] Edit README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26eadc5..152833a 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ For a complete guide, tutorials, and API reference, please visit the **[Official - Fluent API - Flexible Layout System - Rows, Columns & Custom Positioning - - Pixel, Points, 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 From dec1237dd2ec22dae972bc12a6ce9b1cb40cb670 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 11:58:57 -0400 Subject: [PATCH 29/38] Use in keyword with ScalingSettings parameters --- Paper/Paper.Styles.cs | 2 +- Paper/Rounding.cs | 2 +- Paper/ScrollState.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Paper/Paper.Styles.cs b/Paper/Paper.Styles.cs index 3765057..8fe6922 100644 --- a/Paper/Paper.Styles.cs +++ b/Paper/Paper.Styles.cs @@ -430,7 +430,7 @@ public void Update(double deltaTime) /// /// Gets the complete transform for an element. /// - public Transform2D GetTransformForElement(Rect rect, ScalingSettings scalingSettings) + public Transform2D GetTransformForElement(Rect rect, in ScalingSettings scalingSettings) { TransformBuilder builder = new TransformBuilder(); diff --git a/Paper/Rounding.cs b/Paper/Rounding.cs index 1c0ce0f..039f5db 100644 --- a/Paper/Rounding.cs +++ b/Paper/Rounding.cs @@ -24,7 +24,7 @@ public Rounding(in AbsoluteUnit topLeft, in AbsoluteUnit topRight, in AbsoluteUn BottomLeft = bottomLeft; } - public Vector4 ToPx(ScalingSettings scalingSettings) + public Vector4 ToPx(in ScalingSettings scalingSettings) { return new Vector4( TopLeft.ToPx(scalingSettings), TopRight.ToPx(scalingSettings), diff --git a/Paper/ScrollState.cs b/Paper/ScrollState.cs index d8ef1bf..ade7fe7 100644 --- a/Paper/ScrollState.cs +++ b/Paper/ScrollState.cs @@ -69,7 +69,7 @@ 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, ScalingSettings scalingSettings) + 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); @@ -97,7 +97,7 @@ public void ClampScrollPosition() /// /// 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, ScalingSettings scalingSettings) + 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); @@ -125,7 +125,7 @@ public void ClampScrollPosition() /// /// Checks if a point is over the vertical scrollbar. /// - public bool IsPointOverVerticalScrollbar(Vector2 point, Rect rect, Scroll flags, ScalingSettings scalingSettings) + public bool IsPointOverVerticalScrollbar(Vector2 point, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!NeedsVerticalScroll(flags)) return false; @@ -141,7 +141,7 @@ 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, ScalingSettings scalingSettings) + public bool IsPointOverHorizontalScrollbar(Vector2 point, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!NeedsHorizontalScroll(flags)) return false; @@ -157,7 +157,7 @@ public bool IsPointOverHorizontalScrollbar(Vector2 point, Rect rect, Scroll flag /// /// Handles scrollbar dragging for vertical scrollbar. /// - public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, ScalingSettings scalingSettings) + public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!IsDraggingVertical) return; @@ -182,7 +182,7 @@ public void HandleVerticalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flag /// /// Handles scrollbar dragging for horizontal scrollbar. /// - public void HandleHorizontalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, ScalingSettings scalingSettings) + public void HandleHorizontalScrollbarDrag(Vector2 mousePos, Rect rect, Scroll flags, in ScalingSettings scalingSettings) { if (!IsDraggingHorizontal) return; From 0cbd3dca974d3420989941cb8e0b631c6295a7db Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 12:02:26 -0400 Subject: [PATCH 30/38] Fix scrollbar constants being writable --- Paper/ScrollState.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Paper/ScrollState.cs b/Paper/ScrollState.cs index ade7fe7..909d316 100644 --- a/Paper/ScrollState.cs +++ b/Paper/ScrollState.cs @@ -24,10 +24,9 @@ public struct ScrollState public bool IsHorizontalScrollbarHovered; // Constants for scrollbar rendering - // These should be scaled by content scale before use - public static AbsoluteUnit ScrollbarSize = 12; - public static AbsoluteUnit ScrollbarMinSize = 20; - public static AbsoluteUnit 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. From 83a474d8530ac12e24c6e757f0be63838181ecbd Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 12:30:00 -0400 Subject: [PATCH 31/38] Expose ContentScale directly as a convenience property --- Paper/Paper.Core.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 26c4c99..e07907d 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -30,7 +30,6 @@ public partial class Paper private ScalingSettings _scalingSettings = new ScalingSettings(); private Stopwatch _timer = new(); - // Events public Action? OnEndOfFramePreLayout = null; public Action? OnEndOfFramePostLayout = null; @@ -44,6 +43,7 @@ public partial class Paper 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. From acd2fd0118b49a5a0dc926b6e1e36cef691a4ebe Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 12:55:51 -0400 Subject: [PATCH 32/38] Provide ScalingSettings when using AddActionsElement to facilitate scaling user Canvas commands --- Paper/ElementBuilder.cs | 4 ++-- Paper/ElementRenderCommand.cs | 5 +++-- Paper/Paper.Core.cs | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index 4c2e38a..e621aa2 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -1991,7 +1991,7 @@ 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); @@ -1999,7 +1999,7 @@ private ElementBuilder CreateTextInput( canvas.SaveState(); canvas.TransformBy(Transform2D.CreateTranslation(-renderState.ScrollOffsetX, -renderState.ScrollOffsetY)); - var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(_paper.ScalingSettings); + var fontSize = ((AbsoluteUnit)elHandle.Data._elementStyle.GetValue(GuiProp.FontSize)).ToPx(scalingSettings); // Draw text or placeholder if (string.IsNullOrEmpty(renderState.Value)) 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/Paper.Core.cs b/Paper/Paper.Core.cs index e07907d..90426fd 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -371,7 +371,7 @@ private void RenderElement(in ElementHandle handle, Layer currentLayer, List renderAction) + public void AddActionElement(Action renderAction) { var current = CurrentParent; AddActionElement(ref current, renderAction); @@ -655,7 +655,7 @@ public void AddActionElement(Action renderAction) /// /// Adds a custom render action to an element. /// - public void AddActionElement(ref ElementHandle handle, Action renderAction) + public void AddActionElement(ref ElementHandle handle, Action renderAction) { ArgumentNullException.ThrowIfNull(handle); ArgumentNullException.ThrowIfNull(renderAction); From faf40425b66cea3021d52e64af528f61878880f7 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 12:56:23 -0400 Subject: [PATCH 33/38] Update Samples Canvas commands to scale properly --- Samples/Shared/PaperDemo.Components.cs | 4 ++-- Samples/Shared/PaperDemo.cs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Samples/Shared/PaperDemo.Components.cs b/Samples/Shared/PaperDemo.Components.cs index 2054f4e..e6c1f8d 100644 --- a/Samples/Shared/PaperDemo.Components.cs +++ b/Samples/Shared/PaperDemo.Components.cs @@ -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); From fcc506b4ea2d75f381f76e182a7f519d84741dc4 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 13:04:50 -0400 Subject: [PATCH 34/38] Add constructor for ScalingSettings struct --- Paper/LayoutEngine/ScalingSettings.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Paper/LayoutEngine/ScalingSettings.cs b/Paper/LayoutEngine/ScalingSettings.cs index 2291279..f144266 100644 --- a/Paper/LayoutEngine/ScalingSettings.cs +++ b/Paper/LayoutEngine/ScalingSettings.cs @@ -17,6 +17,9 @@ public struct ScalingSettings /// public double ContentScale = 1; - public ScalingSettings() { } + public ScalingSettings(double contentScale = 1) + { + ContentScale = contentScale; + } } } From 70ed5c7c077fc3808a23198ce08d439b746188aa Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 13:05:02 -0400 Subject: [PATCH 35/38] Update test cases --- Tests/AbsoluteUnitTests.cs | 61 ++++++++++++++++++++++++++++++++++++++ Tests/RelativeUnitTests.cs | 61 ++++++++++++++++++++++++++++++++++++++ Tests/UnitValueTests.cs | 59 ------------------------------------ 3 files changed, 122 insertions(+), 59 deletions(-) create mode 100644 Tests/AbsoluteUnitTests.cs create mode 100644 Tests/RelativeUnitTests.cs delete mode 100644 Tests/UnitValueTests.cs 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); - } -} From 0f59de3c25109aaaba388722b18b10da6134b693 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 13:08:48 -0400 Subject: [PATCH 36/38] Document that the Transform property and Canvas commands are not scaled automatically --- Paper/ElementBuilder.cs | 7 ++++++- Paper/Paper.Core.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Paper/ElementBuilder.cs b/Paper/ElementBuilder.cs index e621aa2..1b5191f 100644 --- a/Paper/ElementBuilder.cs +++ b/Paper/ElementBuilder.cs @@ -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 diff --git a/Paper/Paper.Core.cs b/Paper/Paper.Core.cs index 90426fd..0b12414 100644 --- a/Paper/Paper.Core.cs +++ b/Paper/Paper.Core.cs @@ -655,6 +655,9 @@ public void AddActionElement(Action renderAction) /// /// Adds a custom render action to an element. /// + /// + /// Canvas commands are not scaled automatically. Use the provided to apply scaling manually. + /// public void AddActionElement(ref ElementHandle handle, Action renderAction) { ArgumentNullException.ThrowIfNull(handle); From 6fff8372be503cedc0683dbb56c043cce8218e84 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 13:23:07 -0400 Subject: [PATCH 37/38] Add default constructor ScalingSettings to prevent accidental default initialization --- Paper/LayoutEngine/ScalingSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Paper/LayoutEngine/ScalingSettings.cs b/Paper/LayoutEngine/ScalingSettings.cs index f144266..d4cca8c 100644 --- a/Paper/LayoutEngine/ScalingSettings.cs +++ b/Paper/LayoutEngine/ScalingSettings.cs @@ -17,7 +17,9 @@ public struct ScalingSettings /// public double ContentScale = 1; - public ScalingSettings(double contentScale = 1) + public ScalingSettings() { } + + public ScalingSettings(double contentScale) { ContentScale = contentScale; } From 288dcca89f24343caf1a4ac56ce09e89365241f8 Mon Sep 17 00:00:00 2001 From: William Chen Date: Thu, 18 Sep 2025 13:25:23 -0400 Subject: [PATCH 38/38] Update Origami to scale property --- Origami/SpinnerUtil.cs | 55 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 27 deletions(-) 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);