Fixes for handlers

This commit is contained in:
2026-01-24 01:53:26 +00:00
parent f4422d4af1
commit 0c2508d715
7 changed files with 218 additions and 25 deletions

View File

@@ -68,6 +68,13 @@ public partial class LabelHandler : ViewHandler<ILabel, SkiaLabel>
}
}
// Explicitly map LineBreakMode on connect - MAUI may not trigger property change for defaults
if (VirtualView is Microsoft.Maui.Controls.Label mauiLabel)
{
Console.WriteLine($"[LabelHandler] ConnectHandler Text='{mauiLabel.Text?.Substring(0, Math.Min(20, mauiLabel.Text?.Length ?? 0))}...' LineBreakMode={mauiLabel.LineBreakMode} ({(int)mauiLabel.LineBreakMode})");
platformView.LineBreakMode = mauiLabel.LineBreakMode;
}
platformView.Tapped += OnPlatformViewTapped;
}

63
Services/Browser.cs Normal file
View File

@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Static Browser class providing MAUI-compatible API for opening URLs.
/// </summary>
public static class Browser
{
private static IBrowser? _browser;
/// <summary>
/// Gets or sets the browser implementation. Set during app initialization.
/// </summary>
public static IBrowser Default
{
get => _browser ??= new BrowserService();
set => _browser = value;
}
/// <summary>
/// Opens the specified URI in the default browser.
/// </summary>
public static Task<bool> OpenAsync(string uri)
{
return Default.OpenAsync(uri);
}
/// <summary>
/// Opens the specified URI in the default browser with the specified launch mode.
/// </summary>
public static Task<bool> OpenAsync(string uri, BrowserLaunchMode launchMode)
{
return Default.OpenAsync(uri, launchMode);
}
/// <summary>
/// Opens the specified URI in the default browser.
/// </summary>
public static Task<bool> OpenAsync(Uri uri)
{
return Default.OpenAsync(uri);
}
/// <summary>
/// Opens the specified URI in the default browser with the specified launch mode.
/// </summary>
public static Task<bool> OpenAsync(Uri uri, BrowserLaunchMode launchMode)
{
return Default.OpenAsync(uri, launchMode);
}
/// <summary>
/// Opens the specified URI in the default browser with the specified options.
/// </summary>
public static Task<bool> OpenAsync(Uri uri, BrowserLaunchOptions options)
{
return Default.OpenAsync(uri, options);
}
}

View File

@@ -536,11 +536,32 @@ public class SkiaButton : SkiaView, IButtonController
#region Drawing
/// <summary>
/// Override to prevent base class from drawing rectangular background.
/// Button draws its own rounded background in OnDraw.
/// </summary>
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
{
// Don't draw anything - OnDraw handles the rounded background
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
// BackgroundColor is inherited from SkiaView as MAUI Color - convert to SKColor for rendering
var bgColor = GetEffectiveBackgroundColor();
bool hasBackground = bgColor.Alpha > 0;
// Check if BackgroundColor was explicitly set (even if set to transparent)
// This distinguishes "no background specified" from "explicitly transparent"
bool hasExplicitBackground = BackgroundColor != null;
// If no background color is set, use a default button background (like other MAUI platforms)
// This ensures buttons are visible even without explicit styling
if (!hasExplicitBackground)
{
bgColor = SkiaTheme.Gray200SK; // Default button background
}
bool hasBackground = hasExplicitBackground ? bgColor.Alpha > 0 : true;
// Determine current state color
SKColor currentBgColor;
@@ -571,6 +592,10 @@ public class SkiaButton : SkiaView, IButtonController
var roundRect = new SKRoundRect(bounds, cornerRadius);
// Clip to rounded rectangle to prevent background bleeding in corners
canvas.Save();
canvas.ClipRoundRect(roundRect, antialias: true);
// Draw background
if (currentBgColor.Alpha > 0)
{
@@ -612,17 +637,26 @@ public class SkiaButton : SkiaView, IButtonController
}
// Draw content (text and/or image)
DrawContent(canvas, bounds);
DrawContent(canvas, bounds, hasExplicitBackground);
// Restore canvas state (undo clipping)
canvas.Restore();
}
private void DrawContent(SKCanvas canvas, SKRect bounds)
private void DrawContent(SKCanvas canvas, SKRect bounds, bool hasExplicitBackground)
{
var padding = Padding;
// Handle NaN padding (default to 14, 10)
float padLeft = float.IsNaN((float)padding.Left) ? 14f : (float)padding.Left;
float padTop = float.IsNaN((float)padding.Top) ? 10f : (float)padding.Top;
float padRight = float.IsNaN((float)padding.Right) ? 14f : (float)padding.Right;
float padBottom = float.IsNaN((float)padding.Bottom) ? 10f : (float)padding.Bottom;
var contentBounds = new SKRect(
bounds.Left + (float)padding.Left,
bounds.Top + (float)padding.Top,
bounds.Right - (float)padding.Right,
bounds.Bottom - (float)padding.Bottom);
bounds.Left + padLeft,
bounds.Top + padTop,
bounds.Right - padRight,
bounds.Bottom - padBottom);
// Prepare font
bool isBold = FontAttributes.HasFlag(FontAttributes.Bold);
@@ -640,8 +674,24 @@ public class SkiaButton : SkiaView, IButtonController
SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default,
fontSize);
// Prepare text color (null means use platform default: white for buttons)
var textColor = TextColor != null ? ToSKColor(TextColor) : SkiaTheme.BackgroundWhiteSK;
// Prepare text color
// If TextColor is set, use it; otherwise use a sensible default based on background
SKColor textColor;
if (TextColor != null)
{
textColor = ToSKColor(TextColor);
}
else if (hasExplicitBackground)
{
// Explicit background but no text color - use white (common for colored buttons)
textColor = SkiaTheme.BackgroundWhiteSK;
}
else
{
// Default button (gray background) - use dark text for contrast
textColor = SkiaTheme.Gray800SK;
}
if (!IsEnabled)
{
textColor = textColor.WithAlpha(128);
@@ -945,6 +995,11 @@ public class SkiaButton : SkiaView, IButtonController
var padding = Padding;
float paddingH = (float)(padding.Left + padding.Right);
float paddingV = (float)(padding.Top + padding.Bottom);
// Handle NaN padding (can happen with style resolution issues)
if (float.IsNaN(paddingH)) paddingH = 28f; // Default: 14 + 14
if (float.IsNaN(paddingV)) paddingV = 20f; // Default: 10 + 10
float fontSize = FontSize > 0 ? (float)FontSize : 14f;
// Prepare font for measurement
@@ -978,7 +1033,9 @@ public class SkiaButton : SkiaView, IButtonController
{
textWidth += (float)(CharacterSpacing * (displayText.Length - 1));
}
textHeight = textBounds.Height;
// Use font metrics for proper line height (ascent is negative)
var metrics = font.Metrics;
textHeight = metrics.Descent - metrics.Ascent;
}
float imageWidth = 0, imageHeight = 0;
@@ -1037,7 +1094,10 @@ public class SkiaButton : SkiaView, IButtonController
height = (float)HeightRequest;
}
return new Size(Math.Max(width, 44f), Math.Max(height, 30f));
var result = new Size(Math.Max(width, 44f), Math.Max(height, 36f));
if (Text == "Round")
Console.WriteLine($"[SkiaButton.Measure] Text='Round' WReq={WidthRequest} HReq={HeightRequest} width={width:F1} height={height:F1} result={result.Width:F0}x{result.Height:F0}");
return result;
}
#endregion

View File

@@ -921,7 +921,14 @@ public class SkiaEditor : SkiaView, IInputContext
Color = GetEffectivePlaceholderColor(),
IsAntialias = true
};
canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint);
// Handle multiline placeholder text by splitting on newlines
var placeholderLines = Placeholder.Split('\n');
var y = contentRect.Top + fontSize;
foreach (var line in placeholderLines)
{
canvas.DrawText(line, contentRect.Left, y, placeholderPaint);
y += lineSpacing;
}
}
else
{

View File

@@ -361,7 +361,11 @@ public class SkiaStackLayout : SkiaLayoutView
float maxWidth = 0;
float maxHeight = 0;
var childAvailable = new Size(contentWidth, contentHeight);
// For stack layouts, give children infinite size in the stacking direction
// so they can measure to their natural size
var childAvailable = Orientation == StackOrientation.Horizontal
? new Size(double.PositiveInfinity, contentHeight) // Horizontal: infinite width, constrained height
: new Size(contentWidth, double.PositiveInfinity); // Vertical: constrained width, infinite height
foreach (var child in Children)
{
@@ -447,11 +451,9 @@ public class SkiaStackLayout : SkiaLayoutView
}
else
{
// For ScrollView children, give them the remaining viewport width
var remainingWidth = Math.Max(0, contentWidth - offset);
var useWidth = child is SkiaScrollView
? remainingWidth
: Math.Min(childWidth, remainingWidth > 0 ? remainingWidth : childWidth);
// Horizontal stack: give each child its measured width
// Don't constrain - let content overflow if needed (parent clips)
var useWidth = childWidth;
// Respect child's VerticalOptions for horizontal layouts
var useHeight = Math.Min(childHeight, contentHeight);

View File

@@ -295,12 +295,36 @@ public class SkiaScrollView : SkiaView
var contentDesired = _content.Measure(availableSize);
ContentSize = new SKSize((float)contentDesired.Width, (float)contentDesired.Height);
// Apply content's margin
// Apply content's margin and arrange based on scroll orientation
var margin = _content.Margin;
var contentLeft = bounds.Left + (float)margin.Left;
var contentTop = bounds.Top + (float)margin.Top;
var contentWidth = Math.Max(bounds.Width, (float)_content.DesiredSize.Width) - (float)margin.Left - (float)margin.Right;
var contentHeight = Math.Max(bounds.Height, (float)_content.DesiredSize.Height) - (float)margin.Top - (float)margin.Bottom;
// Content dimensions depend on scroll orientation
float contentWidth, contentHeight;
switch (Orientation)
{
case ScrollOrientation.Horizontal:
contentWidth = Math.Max(bounds.Width, (float)_content.DesiredSize.Width);
contentHeight = bounds.Height;
break;
case ScrollOrientation.Neither:
contentWidth = bounds.Width;
contentHeight = bounds.Height;
break;
case ScrollOrientation.Both:
contentWidth = Math.Max(bounds.Width, (float)_content.DesiredSize.Width);
contentHeight = Math.Max(bounds.Height, (float)_content.DesiredSize.Height);
break;
case ScrollOrientation.Vertical:
default:
contentWidth = bounds.Width;
contentHeight = Math.Max(bounds.Height, (float)_content.DesiredSize.Height);
break;
}
contentWidth -= (float)margin.Left + (float)margin.Right;
contentHeight -= (float)margin.Top + (float)margin.Bottom;
var contentBounds = new Rect(contentLeft, contentTop, contentWidth, contentHeight);
_content.Arrange(contentBounds);
@@ -858,12 +882,41 @@ public class SkiaScrollView : SkiaView
if (_content != null)
{
// Apply content's margin and arrange content at its full size
// Apply content's margin and arrange content based on scroll orientation
var margin = _content.Margin;
var contentLeft = (float)actualBounds.Left + (float)margin.Left;
var contentTop = (float)actualBounds.Top + (float)margin.Top;
var contentWidth = Math.Max((float)actualBounds.Width, ContentSize.Width) - (float)margin.Left - (float)margin.Right;
var contentHeight = Math.Max((float)actualBounds.Height, ContentSize.Height) - (float)margin.Top - (float)margin.Bottom;
// Content dimensions depend on scroll orientation:
// - Vertical: width constrained to viewport, height can expand
// - Horizontal: width can expand, height constrained to viewport
// - Both: both can expand
// - Neither: both constrained to viewport
float contentWidth, contentHeight;
switch (Orientation)
{
case ScrollOrientation.Horizontal:
contentWidth = Math.Max((float)actualBounds.Width, ContentSize.Width);
contentHeight = (float)actualBounds.Height;
break;
case ScrollOrientation.Neither:
contentWidth = (float)actualBounds.Width;
contentHeight = (float)actualBounds.Height;
break;
case ScrollOrientation.Both:
contentWidth = Math.Max((float)actualBounds.Width, ContentSize.Width);
contentHeight = Math.Max((float)actualBounds.Height, ContentSize.Height);
break;
case ScrollOrientation.Vertical:
default:
// Vertical scroll: constrain width to viewport, allow height to expand
contentWidth = (float)actualBounds.Width;
contentHeight = Math.Max((float)actualBounds.Height, ContentSize.Height);
break;
}
contentWidth -= (float)margin.Left + (float)margin.Right;
contentHeight -= (float)margin.Top + (float)margin.Bottom;
var contentBounds = new Rect(contentLeft, contentTop, contentWidth, contentHeight);
_content.Arrange(contentBounds);

View File

@@ -109,7 +109,8 @@ public class SkiaSearchBar : SkiaView
BackgroundColor = Colors.Transparent,
BorderColor = Colors.Transparent,
FocusedBorderColor = Colors.Transparent,
BorderWidth = 0
BorderWidth = 0,
VerticalTextAlignment = TextAlignment.Center
};
_entry.TextChanged += (s, e) =>