From afbf8f678226deecbc3f29058d32b3df522c338a Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Sat, 27 Dec 2025 11:20:27 -0500 Subject: [PATCH] Fix layout rendering, text wrapping, and scrollbar issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LinuxViewRenderer: Remove redundant child rendering that caused "View already has a parent" errors - SkiaLabel: Consider LineBreakMode.WordWrap for multi-line measurement and rendering - SkiaScrollView: Update ContentSize in OnDraw with properly constrained measurement - SkiaShell: Account for padding in MeasureOverride (consistent with ArrangeOverride) - Version bump to 1.0.0-preview.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Hosting/LinuxViewRenderer.cs | 95 ++-------------------------------- OpenMaui.Controls.Linux.csproj | 4 +- Views/SkiaLabel.cs | 13 +++-- Views/SkiaScrollView.cs | 18 ++++++- Views/SkiaShell.cs | 6 +-- 5 files changed, 32 insertions(+), 104 deletions(-) diff --git a/Hosting/LinuxViewRenderer.cs b/Hosting/LinuxViewRenderer.cs index f7d4184..a1bac7a 100644 --- a/Hosting/LinuxViewRenderer.cs +++ b/Hosting/LinuxViewRenderer.cs @@ -442,6 +442,7 @@ public class LinuxViewRenderer } // Create handler for the view + // The handler's ConnectHandler and property mappers handle child views automatically var handler = view.ToHandler(_mauiContext); if (handler?.PlatformView is not SkiaView skiaView) @@ -450,98 +451,8 @@ public class LinuxViewRenderer return CreateFallbackView(view); } - // Recursively render children for layout views - if (view is ILayout layout && skiaView is SkiaLayoutView layoutView) - { - - // For StackLayout, copy orientation and spacing - if (layoutView is SkiaStackLayout skiaStack) - { - if (view is Controls.VerticalStackLayout) - { - skiaStack.Orientation = StackOrientation.Vertical; - } - else if (view is Controls.HorizontalStackLayout) - { - skiaStack.Orientation = StackOrientation.Horizontal; - } - else if (view is Controls.StackLayout sl) - { - skiaStack.Orientation = sl.Orientation == Microsoft.Maui.Controls.StackOrientation.Vertical - ? StackOrientation.Vertical : StackOrientation.Horizontal; - } - - if (view is IStackLayout stackLayout) - { - skiaStack.Spacing = (float)stackLayout.Spacing; - } - } - - // For Grid, set up row/column definitions - if (view is Controls.Grid mauiGrid && layoutView is SkiaGrid skiaGrid) - { - // Copy row definitions - foreach (var rowDef in mauiGrid.RowDefinitions) - { - skiaGrid.RowDefinitions.Add(new GridLength((float)rowDef.Height.Value, - rowDef.Height.IsAbsolute ? GridUnitType.Absolute : - rowDef.Height.IsStar ? GridUnitType.Star : GridUnitType.Auto)); - } - // Copy column definitions - foreach (var colDef in mauiGrid.ColumnDefinitions) - { - skiaGrid.ColumnDefinitions.Add(new GridLength((float)colDef.Width.Value, - colDef.Width.IsAbsolute ? GridUnitType.Absolute : - colDef.Width.IsStar ? GridUnitType.Star : GridUnitType.Auto)); - } - skiaGrid.RowSpacing = (float)mauiGrid.RowSpacing; - skiaGrid.ColumnSpacing = (float)mauiGrid.ColumnSpacing; - } - - foreach (var child in layout) - { - if (child is IView childViewElement) - { - var childView = RenderView(childViewElement); - if (childView != null) - { - // For Grid, get attached properties for position - if (layoutView is SkiaGrid grid && child is BindableObject bindable) - { - var row = Controls.Grid.GetRow(bindable); - var col = Controls.Grid.GetColumn(bindable); - var rowSpan = Controls.Grid.GetRowSpan(bindable); - var colSpan = Controls.Grid.GetColumnSpan(bindable); - grid.AddChild(childView, row, col, rowSpan, colSpan); - } - else - { - layoutView.AddChild(childView); - } - } - } - } - } - else if (view is IContentView contentView && contentView.Content is IView contentElement) - { - var content = RenderView(contentElement); - if (content != null) - { - if (skiaView is SkiaBorder border) - { - border.AddChild(content); - } - else if (skiaView is SkiaFrame frame) - { - frame.AddChild(content); - } - else if (skiaView is SkiaScrollView scrollView) - { - scrollView.Content = content; - } - } - } - + // Handlers manage their own children via ConnectHandler and property mappers + // No manual child rendering needed here - that caused "View already has a parent" errors return skiaView; } catch (Exception) diff --git a/OpenMaui.Controls.Linux.csproj b/OpenMaui.Controls.Linux.csproj index 4c1f694..7ae1bf9 100644 --- a/OpenMaui.Controls.Linux.csproj +++ b/OpenMaui.Controls.Linux.csproj @@ -12,7 +12,7 @@ OpenMaui.Controls.Linux - 1.0.0-preview.1 + 1.0.0-preview.4 MarketAlly LLC, David H. Friedel Jr. MarketAlly LLC OpenMaui Linux Controls @@ -23,7 +23,7 @@ https://git.marketally.com/open-maui/maui-linux.git git maui;linux;desktop;skia;gui;cross-platform;dotnet;x11;wayland;openmaui - Initial preview release with 35+ controls and full platform services. + Preview 4: Fixed handler rendering for layouts, text wrapping, and scrollbar measurement issues. README.md icon.png false diff --git a/Views/SkiaLabel.cs b/Views/SkiaLabel.cs index 9945af2..4d2ca5c 100644 --- a/Views/SkiaLabel.cs +++ b/Views/SkiaLabel.cs @@ -429,9 +429,10 @@ public class SkiaLabel : SkiaView bounds.Bottom - Padding.Bottom); // Handle single line vs multiline - // Use DrawSingleLine for normal labels (MaxLines <= 1 or unlimited) without newlines - // Use DrawMultiLineWithWrapping only when MaxLines > 1 (word wrap needed) or text has newlines - bool needsMultiLine = MaxLines > 1 || Text.Contains('\n'); + // Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || + LineBreakMode == LineBreakMode.WordWrap || + LineBreakMode == LineBreakMode.CharacterWrap; if (needsMultiLine) { DrawMultiLineWithWrapping(canvas, paint, font, contentBounds); @@ -771,8 +772,10 @@ public class SkiaLabel : SkiaView using var font = new SKFont(typeface, FontSize); using var paint = new SKPaint(font); - // Use same logic as OnDraw: multiline only when MaxLines > 1 or text has newlines - bool needsMultiLine = MaxLines > 1 || Text.Contains('\n'); + // Use multiline when: MaxLines > 1, text has newlines, OR WordWrap is enabled + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || + LineBreakMode == LineBreakMode.WordWrap || + LineBreakMode == LineBreakMode.CharacterWrap; if (!needsMultiLine) { var textBounds = new SKRect(); diff --git a/Views/SkiaScrollView.cs b/Views/SkiaScrollView.cs index 3aa9887..6f412a7 100644 --- a/Views/SkiaScrollView.cs +++ b/Views/SkiaScrollView.cs @@ -268,8 +268,16 @@ public class SkiaScrollView : SkiaView if (_content != null) { // Ensure content is measured and arranged - var availableSize = new SKSize(bounds.Width, float.PositiveInfinity); - _content.Measure(availableSize); + // Account for vertical scrollbar width to prevent horizontal scrollbar from appearing + var effectiveWidth = bounds.Width; + if (Orientation != ScrollOrientation.Horizontal && VerticalScrollBarVisibility != ScrollBarVisibility.Never) + { + // Reserve space for vertical scrollbar if content might be taller than viewport + effectiveWidth -= ScrollBarWidth; + } + var availableSize = new SKSize(effectiveWidth, float.PositiveInfinity); + // Update ContentSize with the properly constrained measurement + ContentSize = _content.Measure(availableSize); // Apply content's margin var margin = _content.Margin; @@ -669,12 +677,18 @@ public class SkiaScrollView : SkiaView case ScrollOrientation.Both: // For Both: first measure with viewport width to get responsive layout // Content can still exceed viewport if it has minimum width constraints + // Reserve space for vertical scrollbar to prevent horizontal scrollbar contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) + contentWidth -= ScrollBarWidth; contentHeight = float.PositiveInfinity; break; case ScrollOrientation.Vertical: default: + // Reserve space for vertical scrollbar to prevent horizontal scrollbar contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) + contentWidth -= ScrollBarWidth; contentHeight = float.PositiveInfinity; break; } diff --git a/Views/SkiaShell.cs b/Views/SkiaShell.cs index 27eaabd..74a051c 100644 --- a/Views/SkiaShell.cs +++ b/Views/SkiaShell.cs @@ -287,14 +287,14 @@ public class SkiaShell : SkiaLayoutView protected override SKSize MeasureOverride(SKSize availableSize) { - // Measure current content + // Measure current content with padding accounted for (consistent with ArrangeOverride) if (_currentContent != null) { float contentTop = NavBarIsVisible ? NavBarHeight : 0; float contentBottom = TabBarIsVisible ? TabBarHeight : 0; var contentSize = new SKSize( - availableSize.Width, - availableSize.Height - contentTop - contentBottom); + availableSize.Width - (float)Padding.Left - (float)Padding.Right, + availableSize.Height - contentTop - contentBottom - (float)Padding.Top - (float)Padding.Bottom); _currentContent.Measure(contentSize); }