Compare commits

..

No commits in common. "main" and "v1.0.0-preview.3" have entirely different histories.

8 changed files with 111 additions and 125 deletions

View File

@ -1,37 +0,0 @@
# OpenMaui Linux CI/CD Pipeline for Gitea
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_ROOT: C:\dotnet
jobs:
build:
name: Build and Test
runs-on: windows
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore dependencies
run: C:\dotnet\dotnet.exe restore
- name: Build
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
- name: Run tests
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
continue-on-error: true
- name: Pack NuGet (preview)
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg
- name: List NuGet packages
run: dir .\nupkg\

View File

@ -1,46 +0,0 @@
# OpenMaui Linux Release - Publish to NuGet
name: Release to NuGet
on:
release:
types: [published]
push:
tags:
- 'v*'
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_ROOT: C:\dotnet
jobs:
release:
name: Build and Publish to NuGet
runs-on: windows
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version from tag
id: version
shell: pwsh
run: |
$tag = "${{ github.ref_name }}"
$version = $tag -replace '^v', ''
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
echo "Building version: $version"
- name: Restore dependencies
run: C:\dotnet\dotnet.exe restore
- name: Build
run: C:\dotnet\dotnet.exe build --configuration Release --no-restore
- name: Run tests
run: C:\dotnet\dotnet.exe test --configuration Release --no-build --verbosity normal
- name: Pack NuGet package
run: C:\dotnet\dotnet.exe pack --configuration Release --no-build -o ./nupkg /p:PackageVersion=${{ steps.version.outputs.VERSION }}
- name: Publish to NuGet.org
run: C:\dotnet\dotnet.exe nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

View File

@ -442,7 +442,6 @@ public class LinuxViewRenderer
} }
// Create handler for the view // Create handler for the view
// The handler's ConnectHandler and property mappers handle child views automatically
var handler = view.ToHandler(_mauiContext); var handler = view.ToHandler(_mauiContext);
if (handler?.PlatformView is not SkiaView skiaView) if (handler?.PlatformView is not SkiaView skiaView)
@ -451,8 +450,98 @@ public class LinuxViewRenderer
return CreateFallbackView(view); return CreateFallbackView(view);
} }
// Handlers manage their own children via ConnectHandler and property mappers // Recursively render children for layout views
// No manual child rendering needed here - that caused "View already has a parent" errors 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;
}
}
}
return skiaView; return skiaView;
} }
catch (Exception) catch (Exception)

View File

@ -12,18 +12,18 @@
<!-- NuGet Package Properties --> <!-- NuGet Package Properties -->
<PackageId>OpenMaui.Controls.Linux</PackageId> <PackageId>OpenMaui.Controls.Linux</PackageId>
<Version>1.0.0-preview.4</Version> <Version>1.0.0-preview.1</Version>
<Authors>MarketAlly LLC, David H. Friedel Jr.</Authors> <Authors>MarketAlly LLC, David H. Friedel Jr.</Authors>
<Company>MarketAlly LLC</Company> <Company>MarketAlly LLC</Company>
<Product>OpenMaui Linux Controls</Product> <Product>OpenMaui Linux Controls</Product>
<Description>Linux desktop support for .NET MAUI applications using SkiaSharp rendering. Supports X11 and Wayland display servers with 35+ controls, platform services, and accessibility support.</Description> <Description>Linux desktop support for .NET MAUI applications using SkiaSharp rendering. Supports X11 and Wayland display servers with 35+ controls, platform services, and accessibility support.</Description>
<Copyright>Copyright 2025 MarketAlly LLC</Copyright> <Copyright>Copyright 2025 MarketAlly LLC</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.marketally.com/open-maui/maui-linux</PackageProjectUrl> <PackageProjectUrl>https://github.com/open-maui/maui-linux</PackageProjectUrl>
<RepositoryUrl>https://git.marketally.com/open-maui/maui-linux.git</RepositoryUrl> <RepositoryUrl>https://github.com/open-maui/maui-linux.git</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>maui;linux;desktop;skia;gui;cross-platform;dotnet;x11;wayland;openmaui</PackageTags> <PackageTags>maui;linux;desktop;skia;gui;cross-platform;dotnet;x11;wayland;openmaui</PackageTags>
<PackageReleaseNotes>Preview 4: Fixed handler rendering for layouts, text wrapping, and scrollbar measurement issues.</PackageReleaseNotes> <PackageReleaseNotes>Initial preview release with 35+ controls and full platform services.</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild> <GeneratePackageOnBuild>false</GeneratePackageOnBuild>

View File

@ -2,6 +2,7 @@
A comprehensive Linux platform implementation for .NET MAUI using SkiaSharp rendering. A comprehensive Linux platform implementation for .NET MAUI using SkiaSharp rendering.
[![Build Status](https://github.com/open-maui/maui-linux/actions/workflows/ci.yml/badge.svg)](https://github.com/open-maui/maui-linux/actions)
[![NuGet](https://img.shields.io/nuget/v/OpenMaui.Controls.Linux)](https://www.nuget.org/packages/OpenMaui.Controls.Linux) [![NuGet](https://img.shields.io/nuget/v/OpenMaui.Controls.Linux)](https://www.nuget.org/packages/OpenMaui.Controls.Linux)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@ -135,12 +136,12 @@ sudo dnf install libX11-devel libXrandr-devel libXcursor-devel libXi-devel mesa-
## Sample Applications ## Sample Applications
Full sample applications are available in the [maui-linux-samples](https://git.marketally.com/open-maui/maui-linux-samples) repository: Full sample applications are available in the [maui-linux-samples](https://github.com/open-maui/maui-linux-samples) repository:
| Sample | Description | | Sample | Description |
|--------|-------------| |--------|-------------|
| **[TodoApp](https://git.marketally.com/open-maui/maui-linux-samples/src/branch/main/TodoApp)** | Task manager with NavigationPage, XAML data binding, CollectionView | | **[TodoApp](https://github.com/open-maui/maui-linux-samples/tree/main/TodoApp)** | Task manager with NavigationPage, XAML data binding, CollectionView |
| **[ShellDemo](https://git.marketally.com/open-maui/maui-linux-samples/src/branch/main/ShellDemo)** | Control showcase with Shell navigation and flyout menu | | **[ShellDemo](https://github.com/open-maui/maui-linux-samples/tree/main/ShellDemo)** | Control showcase with Shell navigation and flyout menu |
## Quick Example ## Quick Example
@ -179,7 +180,7 @@ app.Run();
## Building from Source ## Building from Source
```bash ```bash
git clone https://git.marketally.com/open-maui/maui-linux.git git clone https://github.com/open-maui/maui-linux.git
cd maui-linux cd maui-linux
dotnet build dotnet build
dotnet test dotnet test
@ -233,7 +234,3 @@ Copyright (c) 2025 MarketAlly LLC. Licensed under the MIT License - see the [LIC
- [SkiaSharp](https://github.com/mono/SkiaSharp) - 2D graphics library - [SkiaSharp](https://github.com/mono/SkiaSharp) - 2D graphics library
- [.NET MAUI](https://github.com/dotnet/maui) - Cross-platform UI framework - [.NET MAUI](https://github.com/dotnet/maui) - Cross-platform UI framework
- The .NET community - The .NET community

View File

@ -429,10 +429,9 @@ public class SkiaLabel : SkiaView
bounds.Bottom - Padding.Bottom); bounds.Bottom - Padding.Bottom);
// Handle single line vs multiline // Handle single line vs multiline
// Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled // Use DrawSingleLine for normal labels (MaxLines <= 1 or unlimited) without newlines
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || // Use DrawMultiLineWithWrapping only when MaxLines > 1 (word wrap needed) or text has newlines
LineBreakMode == LineBreakMode.WordWrap || bool needsMultiLine = MaxLines > 1 || Text.Contains('\n');
LineBreakMode == LineBreakMode.CharacterWrap;
if (needsMultiLine) if (needsMultiLine)
{ {
DrawMultiLineWithWrapping(canvas, paint, font, contentBounds); DrawMultiLineWithWrapping(canvas, paint, font, contentBounds);
@ -772,10 +771,8 @@ public class SkiaLabel : SkiaView
using var font = new SKFont(typeface, FontSize); using var font = new SKFont(typeface, FontSize);
using var paint = new SKPaint(font); using var paint = new SKPaint(font);
// Use multiline when: MaxLines > 1, text has newlines, OR WordWrap is enabled // Use same logic as OnDraw: multiline only when MaxLines > 1 or text has newlines
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || bool needsMultiLine = MaxLines > 1 || Text.Contains('\n');
LineBreakMode == LineBreakMode.WordWrap ||
LineBreakMode == LineBreakMode.CharacterWrap;
if (!needsMultiLine) if (!needsMultiLine)
{ {
var textBounds = new SKRect(); var textBounds = new SKRect();

View File

@ -268,16 +268,8 @@ public class SkiaScrollView : SkiaView
if (_content != null) if (_content != null)
{ {
// Ensure content is measured and arranged // Ensure content is measured and arranged
// Account for vertical scrollbar width to prevent horizontal scrollbar from appearing var availableSize = new SKSize(bounds.Width, float.PositiveInfinity);
var effectiveWidth = bounds.Width; _content.Measure(availableSize);
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 // Apply content's margin
var margin = _content.Margin; var margin = _content.Margin;
@ -677,18 +669,12 @@ public class SkiaScrollView : SkiaView
case ScrollOrientation.Both: case ScrollOrientation.Both:
// For Both: first measure with viewport width to get responsive layout // For Both: first measure with viewport width to get responsive layout
// Content can still exceed viewport if it has minimum width constraints // 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; contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width;
if (VerticalScrollBarVisibility != ScrollBarVisibility.Never)
contentWidth -= ScrollBarWidth;
contentHeight = float.PositiveInfinity; contentHeight = float.PositiveInfinity;
break; break;
case ScrollOrientation.Vertical: case ScrollOrientation.Vertical:
default: default:
// Reserve space for vertical scrollbar to prevent horizontal scrollbar
contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width;
if (VerticalScrollBarVisibility != ScrollBarVisibility.Never)
contentWidth -= ScrollBarWidth;
contentHeight = float.PositiveInfinity; contentHeight = float.PositiveInfinity;
break; break;
} }

View File

@ -287,14 +287,14 @@ public class SkiaShell : SkiaLayoutView
protected override SKSize MeasureOverride(SKSize availableSize) protected override SKSize MeasureOverride(SKSize availableSize)
{ {
// Measure current content with padding accounted for (consistent with ArrangeOverride) // Measure current content
if (_currentContent != null) if (_currentContent != null)
{ {
float contentTop = NavBarIsVisible ? NavBarHeight : 0; float contentTop = NavBarIsVisible ? NavBarHeight : 0;
float contentBottom = TabBarIsVisible ? TabBarHeight : 0; float contentBottom = TabBarIsVisible ? TabBarHeight : 0;
var contentSize = new SKSize( var contentSize = new SKSize(
availableSize.Width - (float)Padding.Left - (float)Padding.Right, availableSize.Width,
availableSize.Height - contentTop - contentBottom - (float)Padding.Top - (float)Padding.Bottom); availableSize.Height - contentTop - contentBottom);
_currentContent.Measure(contentSize); _currentContent.Measure(contentSize);
} }