fix(ui): expose source and inject native pointer movement

Declare the native source surface before the embedded browser without changing its visual column. Replace cursor-only warping with checked SendInput movement after hosted traces showed drag start/completion without deltas; retain physical mode and resize assertions and add source-divider acceptance.

Refs: #8, #9, #10

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
James MaesandCodex committed 2026-10-10 22:00:22 -05:00
1 parent 9ff820f477
commit 4bf0b81b85
6 files changed
+98 -6

No files matched your search

+22
View File
@@ -120,3 +120,25 @@ Fixture-only MARKDOWNVIEWR_INPUT_TRACE=1 records native pressed/released/capture
outline drag events and frontmatter clicks without changing routing or logging content.
This tests whether WinUI receives the exact physical events; no third timing workaround
or weakened outcome assertion. The next exact hosted handles must be observed.
## Native events captured and movement correction
9ff820f4773c54573c1d24028ba489328bf2e9c6 CI 38106466328 loose native/UI
finished 31/34: two prior input failures plus SourceText accessibility lookup.
Build/format/scripts/security/CodeQL passed; installed producer now terminal FAILURE.
Retained exact log: artifacts/native-event-source-ui-38106466328.log.
Trace shows Markdown press handled and release unhandled with no click. Outline
DragStarted and DragCompleted occur, but no DragDelta. The pinned FlaUI 5.0.0
Mouse.Position/MoveTo implementation uses SetCursorPos, while Down/Up use SendInput.
Follow-up injects a real absolute MOUSEEVENTF_MOVE through the existing FlaUI Win32
API, with virtual-desktop bounds/normalization and failure checks. Button states,
exact target checks, 70-pixel resize and all saved-mode/width assertions remain.
Entered/exited/outline-moved traces will distinguish the next delivery path.
Declare native source controls before WebView2 while retaining Grid.Column positions,
as the earlier verified StatusText accessibility fix did. Source test now includes a
real 70-pixel source-divider drag; normalize only native line-ending presentation.
Source/outline resize and frontmatter remain unverified until exact hosted results.
All 9ff820f producers are now terminal; the follow-up may be pushed without cancelling them.
Native paired-find research retained in artifacts/native-find-notes.md; full scope active.
@@ -21,10 +21,13 @@ public sealed partial class MainWindow
}
foreach(UIElement element in new UIElement[] { RootGrid, OutlineDivider, SourceDivider, FrontmatterHide, FrontmatterMetadata, FrontmatterMarkdown })
{
element.AddHandler(UIElement.PointerEnteredEvent, new PointerEventHandler((sender, args) => TracePointer("entered", sender, args)), handledEventsToo: true);
element.AddHandler(UIElement.PointerExitedEvent, new PointerEventHandler((sender, args) => TracePointer("exited", sender, args)), handledEventsToo: true);
element.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler((sender, args) => TracePointer("pressed", sender, args)), handledEventsToo: true);
element.AddHandler(UIElement.PointerReleasedEvent, new PointerEventHandler((sender, args) => TracePointer("released", sender, args)), handledEventsToo: true);
element.AddHandler(UIElement.PointerCaptureLostEvent, new PointerEventHandler((sender, args) => TracePointer("capture-lost", sender, args)), handledEventsToo: true);
}
OutlineDivider.AddHandler(UIElement.PointerMovedEvent, new PointerEventHandler((sender, args) => TracePointer("moved", sender, args)), handledEventsToo: true);
OutlineDivider.DragStarted += (_, args) => App.Log.Info(FormattableString.Invariant($"Input trace: outline drag started; x={args.HorizontalOffset}; y={args.VerticalOffset}."));
OutlineDivider.DragDelta += (_, args) => App.Log.Info(FormattableString.Invariant($"Input trace: outline drag delta; x={args.HorizontalChange}; y={args.VerticalChange}."));
OutlineDivider.DragCompleted += (_, args) => App.Log.Info(FormattableString.Invariant($"Input trace: outline drag completed; canceled={args.Canceled}."));
+1 -1
View File
@@ -96,7 +96,6 @@
</ControlTemplate>
</primitives:Thumb.Template>
</primitives:Thumb>
<WebView2 x:Name="DocumentView" Grid.Column="2" Loaded="DocumentView_Loaded" AutomationProperties.Name="Markdown document" />
<primitives:Thumb x:Name="SourceDivider" Grid.Column="3" Background="{ThemeResource DividerStrokeColorDefaultBrush}" DragDelta="SourceDivider_DragDelta" DragCompleted="SourceDivider_DragCompleted" AutomationProperties.Name="Resize Markdown source">
<primitives:Thumb.Template>
<ControlTemplate TargetType="primitives:Thumb">
@@ -105,6 +104,7 @@
</primitives:Thumb.Template>
</primitives:Thumb>
<TextBox x:Name="SourceText" Grid.Column="4" Visibility="Collapsed" IsReadOnly="True" AcceptsReturn="True" TextWrapping="NoWrap" FontFamily="Consolas" Padding="12" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" AutomationProperties.Name="Markdown source" />
<WebView2 x:Name="DocumentView" Grid.Column="2" Loaded="DocumentView_Loaded" AutomationProperties.Name="Markdown document" />
</Grid>
</Grid>
</Window>
@@ -255,7 +255,7 @@ public sealed class AppSession : IDisposable
{
throw new InvalidOperationException("The native mouse target no longer has a clickable point.");
}
Mouse.Position = point;
NativeMouse.MoveTo(point);
Wait.UntilInputIsProcessed();
AutomationElement? hit = Automation.FromPoint(Mouse.Position);
for(int depth = 0; hit is not null && depth < 8; depth++, hit = hit.Parent)
@@ -215,9 +215,17 @@ public class AppTests
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Source acceptance")), "the document heading", TimeSpan.FromSeconds(30));
Assert.Null(session.FindInApp(cf => cf.ByAutomationId("SourceText")));
session.InvokeMenuItem("View", "SourceToggle");
AutomationElement source = session.WaitForElement("SourceText");
AutomationElement source;
try
{
source = session.WaitForElement("SourceText");
}
catch(TimeoutException exception)
{
throw new TimeoutException($"{exception.Message} Settings: {session.ReadSettings()} Logs: {session.ReadLogs()} Native tree: {session.DescribeDocumentStatus()}", exception);
}
Assert.True(source.Patterns.Value.Pattern.IsReadOnly.Value);
string value = source.Patterns.Value.Pattern.Value.Value.Replace("\r\n", "\n", StringComparison.Ordinal);
string value = source.Patterns.Value.Pattern.Value.Value.ReplaceLineEndings("\n");
Assert.Equal(Markdown, value);
using(var settings = JsonDocument.Parse(session.ReadSettings()))
{
@@ -227,6 +235,25 @@ public class AppTests
File.WriteAllText(Path.Combine(session.DataDirectory, "document.md"), "# Source replacement\n\n**New literal text**");
session.WaitFor(() => source.Patterns.Value.Pattern.Value.Value.Contains("**New literal text**", StringComparison.Ordinal) ? source : null, "the source snapshot after reload");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Source replacement")), "the rendered replacement heading");
AutomationElement divider = session.WaitForElement("SourceDivider");
session.MainWindow.SetForeground();
Assert.True(divider.TryGetClickablePoint(out System.Drawing.Point start));
Assert.Equal("SourceDivider", session.Automation.FromPoint(start).Properties.AutomationId.ValueOrDefault);
NativeMouse.MoveTo(start);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
FlaUI.Core.Input.Mouse.Down(FlaUI.Core.Input.MouseButton.Left);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
NativeMouse.MoveTo(new System.Drawing.Point(start.X - 70, start.Y));
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
FlaUI.Core.Input.Mouse.Up(FlaUI.Core.Input.MouseButton.Left);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
session.WaitFor(
() =>
{
using var settings = JsonDocument.Parse(session.ReadSettings());
return settings.RootElement.GetProperty("SourceWidth").GetDouble() > 360 ? session.MainWindow : null;
},
"the physically resized source width to be saved");
session.InvokeMenuItem("View", "SourceToggle");
session.WaitForElementGone("SourceText");
using var hidden = JsonDocument.Parse(session.ReadSettings());
@@ -254,11 +281,11 @@ public class AppTests
Assert.True(divider.TryGetClickablePoint(out System.Drawing.Point start));
Assert.Equal(session.App.ProcessId, session.Automation.FromPoint(start).Properties.ProcessId.ValueOrDefault);
Assert.Equal("OutlineDivider", session.Automation.FromPoint(start).Properties.AutomationId.ValueOrDefault);
FlaUI.Core.Input.Mouse.Position = start;
NativeMouse.MoveTo(start);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
FlaUI.Core.Input.Mouse.Down(FlaUI.Core.Input.MouseButton.Left);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
FlaUI.Core.Input.Mouse.MoveTo(new System.Drawing.Point(start.X + 70, start.Y));
NativeMouse.MoveTo(new System.Drawing.Point(start.X + 70, start.Y));
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
FlaUI.Core.Input.Mouse.Up(FlaUI.Core.Input.MouseButton.Left);
FlaUI.Core.Input.Wait.UntilInputIsProcessed();
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using FlaUI.Core.WindowsAPI;
namespace MarkdownViewr.Windows.UiTests;
/// <summary>Injects pointer movement through the same Windows input queue used for button events.</summary>
internal static class NativeMouse
{
/// <summary>Uses absolute virtual-desktop coordinates; cursor warping alone did not deliver outline movement in hosted traces.</summary>
public static void MoveTo(Point point)
{
int left = User32.GetSystemMetrics(SystemMetric.SM_XVIRTUALSCREEN);
int top = User32.GetSystemMetrics(SystemMetric.SM_YVIRTUALSCREEN);
int width = User32.GetSystemMetrics(SystemMetric.SM_CXVIRTUALSCREEN);
int height = User32.GetSystemMetrics(SystemMetric.SM_CYVIRTUALSCREEN);
if(width <= 0 || height <= 0 || point.X < left || point.X >= left + width || point.Y < top || point.Y >= top + height)
{
throw new InvalidOperationException("The native mouse target is outside the virtual desktop.");
}
var mouse = new MOUSEINPUT
{
dx = (int)(((long)(point.X - left) * 65536 + 32768) / width),
dy = (int)(((long)(point.Y - top) * 65536 + 32768) / height),
dwFlags = MouseEventFlags.MOUSEEVENTF_MOVE | MouseEventFlags.MOUSEEVENTF_ABSOLUTE | MouseEventFlags.MOUSEEVENTF_VIRTUALDESK,
};
var input = INPUT.MouseInput(mouse);
if(User32.SendInput(1, [input], INPUT.Size) != 1)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows did not accept the native mouse move.");
}
}
}