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; 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 #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) protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{ {
// BackgroundColor is inherited from SkiaView as MAUI Color - convert to SKColor for rendering // BackgroundColor is inherited from SkiaView as MAUI Color - convert to SKColor for rendering
var bgColor = GetEffectiveBackgroundColor(); 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 // Determine current state color
SKColor currentBgColor; SKColor currentBgColor;
@@ -571,6 +592,10 @@ public class SkiaButton : SkiaView, IButtonController
var roundRect = new SKRoundRect(bounds, cornerRadius); 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 // Draw background
if (currentBgColor.Alpha > 0) if (currentBgColor.Alpha > 0)
{ {
@@ -612,17 +637,26 @@ public class SkiaButton : SkiaView, IButtonController
} }
// Draw content (text and/or image) // 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; 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( var contentBounds = new SKRect(
bounds.Left + (float)padding.Left, bounds.Left + padLeft,
bounds.Top + (float)padding.Top, bounds.Top + padTop,
bounds.Right - (float)padding.Right, bounds.Right - padRight,
bounds.Bottom - (float)padding.Bottom); bounds.Bottom - padBottom);
// Prepare font // Prepare font
bool isBold = FontAttributes.HasFlag(FontAttributes.Bold); bool isBold = FontAttributes.HasFlag(FontAttributes.Bold);
@@ -640,8 +674,24 @@ public class SkiaButton : SkiaView, IButtonController
SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default, SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default,
fontSize); fontSize);
// Prepare text color (null means use platform default: white for buttons) // Prepare text color
var textColor = TextColor != null ? ToSKColor(TextColor) : SkiaTheme.BackgroundWhiteSK; // 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) if (!IsEnabled)
{ {
textColor = textColor.WithAlpha(128); textColor = textColor.WithAlpha(128);
@@ -945,6 +995,11 @@ public class SkiaButton : SkiaView, IButtonController
var padding = Padding; var padding = Padding;
float paddingH = (float)(padding.Left + padding.Right); float paddingH = (float)(padding.Left + padding.Right);
float paddingV = (float)(padding.Top + padding.Bottom); 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; float fontSize = FontSize > 0 ? (float)FontSize : 14f;
// Prepare font for measurement // Prepare font for measurement
@@ -978,7 +1033,9 @@ public class SkiaButton : SkiaView, IButtonController
{ {
textWidth += (float)(CharacterSpacing * (displayText.Length - 1)); 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; float imageWidth = 0, imageHeight = 0;
@@ -1037,7 +1094,10 @@ public class SkiaButton : SkiaView, IButtonController
height = (float)HeightRequest; 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 #endregion

View File

@@ -921,7 +921,14 @@ public class SkiaEditor : SkiaView, IInputContext
Color = GetEffectivePlaceholderColor(), Color = GetEffectivePlaceholderColor(),
IsAntialias = true 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 else
{ {

View File

@@ -361,7 +361,11 @@ public class SkiaStackLayout : SkiaLayoutView
float maxWidth = 0; float maxWidth = 0;
float maxHeight = 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) foreach (var child in Children)
{ {
@@ -447,11 +451,9 @@ public class SkiaStackLayout : SkiaLayoutView
} }
else else
{ {
// For ScrollView children, give them the remaining viewport width // Horizontal stack: give each child its measured width
var remainingWidth = Math.Max(0, contentWidth - offset); // Don't constrain - let content overflow if needed (parent clips)
var useWidth = child is SkiaScrollView var useWidth = childWidth;
? remainingWidth
: Math.Min(childWidth, remainingWidth > 0 ? remainingWidth : childWidth);
// Respect child's VerticalOptions for horizontal layouts // Respect child's VerticalOptions for horizontal layouts
var useHeight = Math.Min(childHeight, contentHeight); var useHeight = Math.Min(childHeight, contentHeight);

View File

@@ -295,12 +295,36 @@ public class SkiaScrollView : SkiaView
var contentDesired = _content.Measure(availableSize); var contentDesired = _content.Measure(availableSize);
ContentSize = new SKSize((float)contentDesired.Width, (float)contentDesired.Height); 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 margin = _content.Margin;
var contentLeft = bounds.Left + (float)margin.Left; var contentLeft = bounds.Left + (float)margin.Left;
var contentTop = bounds.Top + (float)margin.Top; 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); var contentBounds = new Rect(contentLeft, contentTop, contentWidth, contentHeight);
_content.Arrange(contentBounds); _content.Arrange(contentBounds);
@@ -858,12 +882,41 @@ public class SkiaScrollView : SkiaView
if (_content != null) 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 margin = _content.Margin;
var contentLeft = (float)actualBounds.Left + (float)margin.Left; var contentLeft = (float)actualBounds.Left + (float)margin.Left;
var contentTop = (float)actualBounds.Top + (float)margin.Top; 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); var contentBounds = new Rect(contentLeft, contentTop, contentWidth, contentHeight);
_content.Arrange(contentBounds); _content.Arrange(contentBounds);

View File

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