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:
6 files changed
+98
-6
No files matched your search
@@ -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}."));
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user