Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 199 additions & 25 deletions .claude/skills/maui-ai-debugging/SKILL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,4 @@ FodyWeavers.xsd

# Local skill version tracking (written by maui-devflow update-skill)
.skill-version
maui-ai-debugging-workspace/
276 changes: 247 additions & 29 deletions src/MauiDevFlow.Agent.Core/DevFlowAgentService.cs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/MauiDevFlow.Agent.Core/MauiDevFlow.Agent.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Fizzler" Version="1.3.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="SkiaSharp" Version="3.119.0" />
</ItemGroup>

<ItemGroup>
Expand Down
19 changes: 19 additions & 0 deletions src/MauiDevFlow.Agent.Core/VisualTreeWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,25 @@ private ElementInfo CreateElementInfo(IVisualTreeElement element, string id, str
info.Gestures = gestures;
}

// Populate ItemsView metadata (item count for CollectionView/ListView/CarouselView)
if (element is ItemsView itemsView)
{
info.NativeProperties ??= new Dictionary<string, string?>();
try
{
if (itemsView.ItemsSource != null)
{
var count = itemsView.ItemsSource switch
{
System.Collections.ICollection c => c.Count,
_ => itemsView.ItemsSource.Cast<object>().Count()
};
info.NativeProperties["itemCount"] = count.ToString();
}
}
catch { /* ItemsSource may not support counting */ }
}

return info;
}

Expand Down
62 changes: 62 additions & 0 deletions src/MauiDevFlow.Agent.Gtk/GtkAgentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,35 @@ public GtkAgentService(AgentOptions? options = null) : base(options) { }
protected override string DeviceTypeName => "Virtual";
protected override string IdiomName => "Desktop";

protected override double GetWindowDisplayDensity(IWindow? window)
{
try
{
// GTK4: get the scale factor from the native Gtk.Window's display/surface
if (window?.Handler?.PlatformView is global::Gtk.Window gtkWindow)
{
var surface = gtkWindow.GetSurface();
if (surface != null)
return surface.GetScaleFactor();
}

// Fallback: walk widget hierarchy to find the Gtk.Window
if (window is Microsoft.Maui.Controls.Window mauiWindow)
{
if (mauiWindow.Page is Shell shell && shell.CurrentPage?.Handler?.PlatformView is global::Gtk.Widget cpWidget)
{
var root = cpWidget.GetRoot();
if (root is global::Gtk.Widget rootWidget)
return rootWidget.GetScaleFactor();
}
if (mauiWindow.Page?.Handler?.PlatformView is global::Gtk.Widget pageWidget)
return pageWidget.GetScaleFactor();
}
}
catch { }
return 1.0;
}

protected override (double width, double height) GetNativeWindowSize(IWindow window)
{
try
Expand Down Expand Up @@ -48,6 +77,39 @@ protected override (double width, double height) GetNativeWindowSize(IWindow win
return base.GetNativeWindowSize(window);
}

protected override Task<bool> TryNativeScroll(VisualElement element, double deltaX, double deltaY)
{
try
{
var target = element;
while (target != null)
{
if (target.Handler?.PlatformView is global::Gtk.Widget widget)
{
// Walk up GTK widget hierarchy looking for ScrolledWindow
var current = widget;
while (current != null)
{
if (current is global::Gtk.ScrolledWindow scrolledWindow)
{
var hAdj = scrolledWindow.GetHadjustment();
var vAdj = scrolledWindow.GetVadjustment();
if (hAdj != null && deltaX != 0)
hAdj.SetValue(Math.Max(hAdj.GetLower(), Math.Min(hAdj.GetValue() + deltaX, hAdj.GetUpper() - hAdj.GetPageSize())));
if (vAdj != null && deltaY != 0)
vAdj.SetValue(Math.Max(vAdj.GetLower(), Math.Min(vAdj.GetValue() - deltaY, vAdj.GetUpper() - vAdj.GetPageSize())));
return Task.FromResult(true);
}
current = current.GetParent() as global::Gtk.Widget;
}
}
target = target.Parent as VisualElement;
}
}
catch { }
return Task.FromResult(false);
}

protected override bool TryNativeTap(VisualElement ve)
{
try
Expand Down
193 changes: 193 additions & 0 deletions src/MauiDevFlow.Agent/DevFlowAgentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,199 @@

protected override VisualTreeWalker CreateTreeWalker() => new PlatformVisualTreeWalker();

protected override double GetWindowDisplayDensity(IWindow? window)
{
try
{
#if IOS || MACCATALYST
if (window?.Handler?.PlatformView is UIKit.UIWindow uiWindow)
return uiWindow.Screen.Scale;
return UIKit.UIScreen.MainScreen.Scale;
#elif ANDROID
if (window?.Handler?.PlatformView is Android.App.Activity activity)
return activity.Resources?.DisplayMetrics?.Density ?? 1.0;
if (Android.App.Application.Context.Resources?.DisplayMetrics is Android.Util.DisplayMetrics dm)
return dm.Density;
return 1.0;
#elif WINDOWS
if (window?.Handler?.PlatformView is Microsoft.UI.Xaml.Window winuiWindow)
{
var xamlRoot = winuiWindow.Content?.XamlRoot;
if (xamlRoot != null)
return xamlRoot.RasterizationScale;
}
return 1.0;
#elif MACOS
if (window?.Handler?.PlatformView is AppKit.NSWindow nsWindow)
return nsWindow.BackingScaleFactor;
return AppKit.NSScreen.MainScreen?.BackingScaleFactor ?? 2.0;
#else
return base.GetWindowDisplayDensity(window);
#endif
}
catch
{
return base.GetWindowDisplayDensity(window);
}
}

protected override Task<bool> TryNativeScroll(VisualElement element, double deltaX, double deltaY)
{
try
{
// Walk up from the element to find a native scrollable view
var target = element;
while (target != null)
{
var platformView = target.Handler?.PlatformView;
if (platformView != null)
{
#if IOS || MACCATALYST
// Check: view itself → subviews → ancestors
var uiView = platformView as UIKit.UIView;
UIKit.UIScrollView? uiScrollView = uiView as UIKit.UIScrollView;
if (uiScrollView == null)
uiScrollView = FindNativeDescendant<UIKit.UIScrollView>(uiView);
if (uiScrollView == null)
uiScrollView = FindNativeAncestor<UIKit.UIScrollView>(uiView);
if (uiScrollView != null)
{
var offset = uiScrollView.ContentOffset;
var newX = Math.Max(0, Math.Min(offset.X + deltaX, uiScrollView.ContentSize.Width - uiScrollView.Bounds.Width));
var newY = Math.Max(0, Math.Min(offset.Y - deltaY, uiScrollView.ContentSize.Height - uiScrollView.Bounds.Height));
uiScrollView.SetContentOffset(new CoreGraphics.CGPoint(newX, newY), animated: true);
return Task.FromResult(true);
}
#elif ANDROID
// Check: view itself → descendants → ancestors
var androidView = platformView as Android.Views.View;
var recyclerView = androidView as AndroidX.RecyclerView.Widget.RecyclerView;
if (recyclerView == null)
recyclerView = FindNativeDescendantAndroid<AndroidX.RecyclerView.Widget.RecyclerView>(androidView);
if (recyclerView == null)
recyclerView = FindNativeAncestorAndroid<AndroidX.RecyclerView.Widget.RecyclerView>(androidView);
if (recyclerView != null)
{
recyclerView.ScrollBy((int)deltaX, (int)-deltaY);
return Task.FromResult(true);
}
var androidScrollView = androidView as Android.Widget.ScrollView;
if (androidScrollView == null)
androidScrollView = FindNativeDescendantAndroid<Android.Widget.ScrollView>(androidView);
if (androidScrollView == null)
androidScrollView = FindNativeAncestorAndroid<Android.Widget.ScrollView>(androidView);
if (androidScrollView != null)
{
androidScrollView.ScrollBy((int)deltaX, (int)-deltaY);
return Task.FromResult(true);
}
#elif WINDOWS
// Check: view itself → descendants → ancestors
var winView = platformView as Microsoft.UI.Xaml.DependencyObject;
var scrollViewer = winView as Microsoft.UI.Xaml.Controls.ScrollViewer;
if (scrollViewer == null)
scrollViewer = FindWinUIDescendant<Microsoft.UI.Xaml.Controls.ScrollViewer>(winView);
if (scrollViewer == null)
scrollViewer = FindWinUIScrollViewer(winView);
if (scrollViewer != null)
{
scrollViewer.ChangeView(
scrollViewer.HorizontalOffset + deltaX,
scrollViewer.VerticalOffset - deltaY,
null);
return Task.FromResult(true);
}
#endif
}
target = target.Parent as VisualElement;
}
}
catch { }
return Task.FromResult(false);
}

#if IOS || MACCATALYST
private static T? FindNativeAncestor<T>(UIKit.UIView? view) where T : UIKit.UIView
{
var current = view;
while (current != null)
{
if (current is T match) return match;
current = current.Superview;
}
return null;
}

private static T? FindNativeDescendant<T>(UIKit.UIView? view) where T : UIKit.UIView
{
if (view == null) return null;
if (view is T match) return match;
foreach (var subview in view.Subviews)
{
var found = FindNativeDescendant<T>(subview);
if (found != null) return found;
}
return null;
}
#elif ANDROID
private static T? FindNativeAncestorAndroid<T>(Android.Views.View? view) where T : Android.Views.View
{
var current = view;
while (current != null)
{
if (current is T match) return match;
current = current.Parent as Android.Views.View;
}
return null;
}

private static T? FindNativeDescendantAndroid<T>(Android.Views.View? view) where T : Android.Views.View
{
if (view == null) return null;
if (view is T match) return match;
if (view is Android.Views.ViewGroup vg)
{
for (var i = 0; i < vg.ChildCount; i++)
{
var found = FindNativeDescendantAndroid<T>(vg.GetChildAt(i));
if (found != null) return found;
}
}
return null;
}
#elif WINDOWS
private static Microsoft.UI.Xaml.Controls.ScrollViewer? FindWinUIScrollViewer(Microsoft.UI.Xaml.DependencyObject? obj)
{
if (obj == null) return null;
if (obj is Microsoft.UI.Xaml.Controls.ScrollViewer sv) return sv;
// Walk up the visual tree
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(obj);
while (parent != null)
{
if (parent is Microsoft.UI.Xaml.Controls.ScrollViewer scrollViewer)
return scrollViewer;
parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(parent);
}
// Also search children (CollectionView wraps a ScrollViewer internally)
return FindWinUIDescendant<Microsoft.UI.Xaml.Controls.ScrollViewer>(obj);
}

private static T? FindWinUIDescendant<T>(Microsoft.UI.Xaml.DependencyObject? parent) where T : Microsoft.UI.Xaml.DependencyObject
{
if (parent == null) return null;
if (parent is T match) return match;
var count = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(parent);
for (var i = 0; i < count; i++)
{
var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(parent, i);
if (child is T found) return found;
var descendant = FindWinUIDescendant<T>(child);
if (descendant != null) return descendant;
}
return null;
}
#endif

protected override bool TryNativeTap(VisualElement ve)
{
try
Expand All @@ -39,10 +232,10 @@
#elif MACOS
if (platformView is NSButton button)
{
button.PerformClick(null);

Check warning on line 235 in src/MauiDevFlow.Agent/DevFlowAgentService.cs

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Cannot convert null literal to non-nullable reference type.
return true;
}
if (platformView is NSControl nsControl && nsControl.Action != null)

Check warning on line 238 in src/MauiDevFlow.Agent/DevFlowAgentService.cs

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 238 in src/MauiDevFlow.Agent/DevFlowAgentService.cs

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Possible null reference argument for parameter 'left' in 'bool Selector.operator !=(Selector left, Selector right)'.
{
nsControl.SendAction(nsControl.Action, nsControl.Target);
return true;
Expand Down
Loading
Loading