Fix Views: SkiaEntry, SkiaEditor, SkiaShell, SkiaWebView

- SkiaEntry.cs: TextProperty BindingMode.OneWay (was TwoWay)
- SkiaEditor.cs: All BindingModes corrected (Text=OneWay, others=TwoWay)
- SkiaShell.cs: Added FlyoutTextColor, ContentBackgroundColor properties,
  route registration system, query parameter support, OnScroll handler
- SkiaWebView.cs: Full rewrite with X11 embedding, GTK window positioning,
  hardware acceleration settings, load-changed callbacks, position tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 14:27:33 -05:00
parent 5613df6031
commit 55d4a6eaad
5 changed files with 1194 additions and 241 deletions

View File

@@ -21,7 +21,7 @@ public class SkiaEditor : SkiaView
typeof(string),
typeof(SkiaEditor),
"",
BindingMode.TwoWay,
BindingMode.OneWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).OnTextPropertyChanged((string)o, (string)n));
/// <summary>
@@ -33,6 +33,7 @@ public class SkiaEditor : SkiaView
typeof(string),
typeof(SkiaEditor),
"",
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -44,6 +45,7 @@ public class SkiaEditor : SkiaView
typeof(SKColor),
typeof(SkiaEditor),
SKColors.Black,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -55,6 +57,7 @@ public class SkiaEditor : SkiaView
typeof(SKColor),
typeof(SkiaEditor),
new SKColor(0x80, 0x80, 0x80),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -66,6 +69,7 @@ public class SkiaEditor : SkiaView
typeof(SKColor),
typeof(SkiaEditor),
new SKColor(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -77,6 +81,7 @@ public class SkiaEditor : SkiaView
typeof(SKColor),
typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3, 0x60),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -88,6 +93,7 @@ public class SkiaEditor : SkiaView
typeof(SKColor),
typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -99,6 +105,7 @@ public class SkiaEditor : SkiaView
typeof(string),
typeof(SkiaEditor),
"Sans",
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
@@ -110,6 +117,7 @@ public class SkiaEditor : SkiaView
typeof(float),
typeof(SkiaEditor),
14f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
@@ -121,6 +129,7 @@ public class SkiaEditor : SkiaView
typeof(float),
typeof(SkiaEditor),
1.4f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
@@ -132,6 +141,7 @@ public class SkiaEditor : SkiaView
typeof(float),
typeof(SkiaEditor),
4f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -143,6 +153,7 @@ public class SkiaEditor : SkiaView
typeof(float),
typeof(SkiaEditor),
12f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
@@ -154,6 +165,7 @@ public class SkiaEditor : SkiaView
typeof(bool),
typeof(SkiaEditor),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
@@ -164,7 +176,8 @@ public class SkiaEditor : SkiaView
nameof(MaxLength),
typeof(int),
typeof(SkiaEditor),
-1);
-1,
BindingMode.TwoWay);
/// <summary>
/// Bindable property for AutoSize.
@@ -175,6 +188,7 @@ public class SkiaEditor : SkiaView
typeof(bool),
typeof(SkiaEditor),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
#endregion

View File

@@ -24,7 +24,7 @@ public class SkiaEntry : SkiaView
typeof(string),
typeof(SkiaEntry),
"",
BindingMode.TwoWay,
BindingMode.OneWay,
propertyChanged: (b, o, n) => ((SkiaEntry)b).OnTextPropertyChanged((string)o, (string)n));
/// <summary>

View File

@@ -22,7 +22,7 @@ public class SkiaShell : SkiaLayoutView
typeof(bool),
typeof(SkiaShell),
false,
BindingMode.TwoWay,
BindingMode.OneWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutIsPresentedChanged((bool)n));
/// <summary>
@@ -34,6 +34,7 @@ public class SkiaShell : SkiaLayoutView
typeof(ShellFlyoutBehavior),
typeof(SkiaShell),
ShellFlyoutBehavior.Flyout,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
@@ -45,6 +46,7 @@ public class SkiaShell : SkiaLayoutView
typeof(float),
typeof(SkiaShell),
280f,
BindingMode.TwoWay,
coerceValue: (b, v) => Math.Max(100f, (float)v),
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
@@ -57,6 +59,19 @@ public class SkiaShell : SkiaLayoutView
typeof(SKColor),
typeof(SkiaShell),
SKColors.White,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
/// Bindable property for FlyoutTextColor.
/// </summary>
public static readonly BindableProperty FlyoutTextColorProperty =
BindableProperty.Create(
nameof(FlyoutTextColor),
typeof(SKColor),
typeof(SkiaShell),
new SKColor(33, 33, 33),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
@@ -68,6 +83,7 @@ public class SkiaShell : SkiaLayoutView
typeof(SKColor),
typeof(SkiaShell),
new SKColor(33, 150, 243),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
@@ -79,6 +95,7 @@ public class SkiaShell : SkiaLayoutView
typeof(SKColor),
typeof(SkiaShell),
SKColors.White,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
@@ -90,6 +107,7 @@ public class SkiaShell : SkiaLayoutView
typeof(float),
typeof(SkiaShell),
56f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
/// <summary>
@@ -101,6 +119,7 @@ public class SkiaShell : SkiaLayoutView
typeof(float),
typeof(SkiaShell),
56f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
/// <summary>
@@ -112,6 +131,7 @@ public class SkiaShell : SkiaLayoutView
typeof(bool),
typeof(SkiaShell),
true,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
/// <summary>
@@ -123,6 +143,7 @@ public class SkiaShell : SkiaLayoutView
typeof(bool),
typeof(SkiaShell),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
/// <summary>
@@ -133,9 +154,22 @@ public class SkiaShell : SkiaLayoutView
nameof(ContentPadding),
typeof(float),
typeof(SkiaShell),
16f,
0f,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure());
/// <summary>
/// Bindable property for ContentBackgroundColor.
/// </summary>
public static readonly BindableProperty ContentBackgroundColorProperty =
BindableProperty.Create(
nameof(ContentBackgroundColor),
typeof(SKColor),
typeof(SkiaShell),
new SKColor(250, 250, 250),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
/// <summary>
/// Bindable property for Title.
/// </summary>
@@ -145,6 +179,7 @@ public class SkiaShell : SkiaLayoutView
typeof(string),
typeof(SkiaShell),
string.Empty,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate());
#endregion
@@ -158,6 +193,10 @@ public class SkiaShell : SkiaLayoutView
// Navigation stack for push/pop navigation
private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new();
private float _flyoutScrollOffset;
private readonly Dictionary<string, Func<SkiaView?>> _registeredRoutes = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _routeTitles = new(StringComparer.OrdinalIgnoreCase);
private void OnFlyoutIsPresentedChanged(bool newValue)
{
_flyoutAnimationProgress = newValue ? 1f : 0f;
@@ -201,6 +240,35 @@ public class SkiaShell : SkiaLayoutView
set => SetValue(FlyoutBackgroundColorProperty, value);
}
/// <summary>
/// Text color in the flyout.
/// </summary>
public SKColor FlyoutTextColor
{
get => (SKColor)GetValue(FlyoutTextColorProperty);
set => SetValue(FlyoutTextColorProperty, value);
}
/// <summary>
/// Optional header view in the flyout.
/// </summary>
public SkiaView? FlyoutHeaderView { get; set; }
/// <summary>
/// Height of the flyout header.
/// </summary>
public float FlyoutHeaderHeight { get; set; } = 140f;
/// <summary>
/// Optional footer text in the flyout.
/// </summary>
public string? FlyoutFooterText { get; set; }
/// <summary>
/// Height of the flyout footer.
/// </summary>
public float FlyoutFooterHeight { get; set; } = 40f;
/// <summary>
/// Background color of the navigation bar.
/// </summary>
@@ -257,7 +325,6 @@ public class SkiaShell : SkiaLayoutView
/// <summary>
/// Gets or sets the padding applied to page content.
/// Default is 16 pixels on all sides.
/// </summary>
public float ContentPadding
{
@@ -265,6 +332,15 @@ public class SkiaShell : SkiaLayoutView
set => SetValue(ContentPaddingProperty, value);
}
/// <summary>
/// Background color of the content area.
/// </summary>
public SKColor ContentBackgroundColor
{
get => (SKColor)GetValue(ContentBackgroundColorProperty);
set => SetValue(ContentBackgroundColorProperty, value);
}
/// <summary>
/// Current title displayed in the navigation bar.
/// </summary>
@@ -404,34 +480,161 @@ public class SkiaShell : SkiaLayoutView
/// </summary>
public void GoToAsync(string route)
{
// Simple route parsing - format: "//section/item"
GoToAsync(route, null);
}
/// <summary>
/// Navigates using a URI route with parameters.
/// </summary>
public void GoToAsync(string route, IDictionary<string, object>? parameters)
{
if (string.IsNullOrEmpty(route)) return;
var parts = route.TrimStart('/').Split('/');
string routePath = route;
Dictionary<string, string> queryParams = new Dictionary<string, string>();
int queryIndex = route.IndexOf('?');
if (queryIndex >= 0)
{
routePath = route.Substring(0, queryIndex);
queryParams = ParseQueryString(route.Substring(queryIndex + 1));
}
Dictionary<string, object> allParams = new Dictionary<string, object>();
foreach (var kvp in queryParams)
{
allParams[kvp.Key] = kvp.Value;
}
if (parameters != null)
{
foreach (var kvp in parameters)
{
allParams[kvp.Key] = kvp.Value;
}
}
var parts = routePath.TrimStart('/').Split('/');
if (parts.Length == 0) return;
// Check registered routes first
if (_registeredRoutes.TryGetValue(routePath.TrimStart('/'), out Func<SkiaView?>? factory))
{
var view = factory();
if (view != null)
{
ApplyQueryParameters(view, allParams);
PushAsync(view, GetRouteTitle(routePath.TrimStart('/')));
return;
}
}
// Find matching section
for (int i = 0; i < _sections.Count; i++)
{
var section = _sections[i];
if (section.Route.Equals(parts[0], StringComparison.OrdinalIgnoreCase))
if (!section.Route.Equals(parts[0], StringComparison.OrdinalIgnoreCase))
continue;
if (parts.Length > 1)
{
if (parts.Length > 1)
// Find matching item
for (int j = 0; j < section.Items.Count; j++)
{
// Find matching item
for (int j = 0; j < section.Items.Count; j++)
if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase))
{
if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase))
NavigateToSection(i, j);
if (section.Items[j].Content != null && allParams.Count > 0)
{
NavigateToSection(i, j);
return;
ApplyQueryParameters(section.Items[j].Content!, allParams);
}
return;
}
}
NavigateToSection(i, 0);
return;
}
NavigateToSection(i);
if (section.Items.Count > 0 && section.Items[0].Content != null && allParams.Count > 0)
{
ApplyQueryParameters(section.Items[0].Content!, allParams);
}
break;
}
}
private static Dictionary<string, string> ParseQueryString(string queryString)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(queryString)) return result;
var pairs = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var parts = pair.Split('=', 2);
if (parts.Length == 2)
{
result[Uri.UnescapeDataString(parts[0])] = Uri.UnescapeDataString(parts[1]);
}
else if (parts.Length == 1)
{
result[Uri.UnescapeDataString(parts[0])] = string.Empty;
}
}
return result;
}
private static void ApplyQueryParameters(SkiaView content, IDictionary<string, object> parameters)
{
if (parameters.Count == 0) return;
if (content is ISkiaQueryAttributable attributable)
{
attributable.ApplyQueryAttributes(parameters);
}
var type = content.GetType();
foreach (var param in parameters)
{
var prop = type.GetProperty(param.Key, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (prop != null && prop.CanWrite)
{
try
{
var value = Convert.ChangeType(param.Value, prop.PropertyType);
prop.SetValue(content, value);
}
catch { }
}
}
}
/// <summary>
/// Registers a route with a content factory.
/// </summary>
public void RegisterRoute(string route, Func<SkiaView?> contentFactory, string? title = null)
{
var key = route.TrimStart('/');
_registeredRoutes[key] = contentFactory;
if (!string.IsNullOrEmpty(title))
{
_routeTitles[key] = title;
}
}
/// <summary>
/// Unregisters a route.
/// </summary>
public void UnregisterRoute(string route)
{
var key = route.TrimStart('/');
_registeredRoutes.Remove(key);
_routeTitles.Remove(key);
}
private string GetRouteTitle(string route)
{
if (_routeTitles.TryGetValue(route, out string? title))
{
return title;
}
return route.Split('/').LastOrDefault() ?? route;
}
/// <summary>
@@ -884,6 +1087,32 @@ public class SkiaShell : SkiaLayoutView
base.OnPointerPressed(e);
}
public override void OnScroll(ScrollEventArgs e)
{
if (FlyoutIsPresented && _flyoutAnimationProgress > 0)
{
float flyoutX = Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress);
var flyoutBounds = new SKRect(flyoutX, Bounds.Top, flyoutX + FlyoutWidth, Bounds.Bottom);
if (flyoutBounds.Contains(e.X, e.Y))
{
float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f;
float footerHeight = !string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f;
float itemHeight = 48f;
float totalItemsHeight = _sections.Count * itemHeight;
float viewableHeight = flyoutBounds.Height - headerHeight - footerHeight;
float maxScroll = Math.Max(0f, totalItemsHeight - viewableHeight);
_flyoutScrollOffset -= e.DeltaY * 30f;
_flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, maxScroll));
Invalidate();
e.Handled = true;
return;
}
}
base.OnScroll(e);
}
}
/// <summary>

View File

File diff suppressed because it is too large Load Diff