Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8d466e9
Constrain Mono autocomplete list width, height and screen position
PawelTroka Oct 26, 2026
b53ef49
Keep Mono dropdown at measured size and protect focus transitions
PawelTroka Oct 26, 2026
815139e
Make Mono suggestions scrollable and reliably clickable
PawelTroka Oct 26, 2026
43e03b4
Place and size Mono function references within the visible work area
PawelTroka Oct 26, 2026
6fa810a
Use a readable native font for Mono function references
PawelTroka Oct 26, 2026
5e30c60
Exercise Mono autocomplete with real X11 typing, pointer selection an…
PawelTroka Oct 26, 2026
6f89600
Include native Mono autocomplete GUI acceptance in the test suite
PawelTroka Oct 26, 2026
9dd1f58
Install X11 input driver for native autocomplete acceptance
PawelTroka Oct 26, 2026
e63efa7
Clear stale autocomplete screenshot before each Mono acceptance run
PawelTroka Oct 26, 2026
37564f3
Document X11 GUI regression-test dependency
PawelTroka Oct 26, 2026
9d94d73
Rely on ToolStripDropDown native no-activation behavior
PawelTroka Oct 26, 2026
42021fa
Rely on ToolStripDropDown native no-activation behavior
PawelTroka Oct 26, 2026
2708318
Set explicit X11 input focus in headless autocomplete acceptance and …
PawelTroka Oct 26, 2026
0ae3ee2
Accept native monochrome WinForms screenshots without weakening inter…
PawelTroka Oct 26, 2026
70bd467
Capture full X11 framebuffer and native mapped-window tree for popup …
PawelTroka Oct 26, 2026
e97144e
Install xwininfo for native X11 popup mapping diagnostics
PawelTroka Oct 26, 2026
a725f14
Fix Mono hosted autocomplete list centered outside its dropdown bounds
PawelTroka Oct 26, 2026
d97c213
Record X11 popup map states alongside native layout bounds
PawelTroka Oct 26, 2026
a21a54f
fix(mono): include MonoAutocompleteTests.cs in developer solution inv…
PawelTroka Oct 26, 2026
2394ffc
test(mono): require real painted autocomplete and help popup pixels i…
PawelTroka Oct 26, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/mono-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install the actual Mono and X11 runtime
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends mono-devel xvfb xauth libgdiplus libgsl-dev fonts-dejavu-core
sudo apt-get install -y --no-install-recommends mono-devel xvfb xauth xdotool x11-utils libgdiplus libgsl-dev fonts-dejavu-core
- name: Build the opt-in Framework graph and run desktop acceptance
run: ./build.sh --target=Mono-Validate
- name: Retain runtime evidence even on failure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ internal AutocompleteListView()
formTip = new WebBrowserForm();
Colors = new Colors();
AutoScaleMode=AutoScaleMode.Font;
#if __MonoCS__
AutoSize = false;
AutoScroll = true;
#else
AutoSize = true;
#endif
}


Expand Down Expand Up @@ -113,6 +118,7 @@ public IList<AutocompleteItem> VisibleItems
{
visibleItems = value;
SelectedItemIndex = -1;
oldItemCount = -1;
AdjustScroll();
Invalidate();
}
Expand All @@ -134,7 +140,15 @@ public int SelectedItemIndex

if (item != null)
{
#if __MonoCS__
// Filtering selects an item before the menu has been placed.
// Showing the help at that point can steal focus or put it
// at an unrelated screen origin on Mono.
if (Parent is AutocompleteMenuHost host && host.Visible)
ShowToolTip(item);
#else
ShowToolTip(item);
#endif
ScrollToSelected();
}

Expand Down Expand Up @@ -338,9 +352,17 @@ protected override void OnMouseClick(MouseEventArgs e)

if (e.Button == MouseButtons.Left)
{
SelectedItemIndex = PointToItemIndex(e.Location);
var index = PointToItemIndex(e.Location);
if (index < 0 || VisibleItems == null || index >= VisibleItems.Count)
return;
SelectedItemIndex = index;
ScrollToSelected();
Invalidate();
#if __MonoCS__
// A Mono popup can lose its focus between two clicks, so the
// usual Windows double-click-to-accept isn't dependable.
OnItemSelected();
#endif
}
}

Expand All @@ -364,9 +386,11 @@ protected override void OnMouseMove(MouseEventArgs e)
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
#if !__MonoCS__
SelectedItemIndex = PointToItemIndex(e.Location);
Invalidate();
OnItemSelected();
#endif
}

private void OnItemSelected()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ public AutocompleteMenu(ISharedViewState sharedViewState)
Enabled = true;
AppearInterval = 500;
_timer.Tick += Timer_Tick;
#if __MonoCS__
// On Mono a 2,000-pixel completion menu obscures the editor and
// leaves no room for its function reference. Size to the items.
var preferred = new Size(580, 360).DpiScale();
#else
var preferred = new Size(2000, 420).DpiScale();
#endif
var available = Screen.FromControl(Host).WorkingArea.Size;
MaximumSize = new Size(Math.Min(preferred.Width, Math.Max(1, available.Width - 4)),
Math.Min(preferred.Height, Math.Max(1, available.Height - 4)));
Expand Down Expand Up @@ -577,6 +583,12 @@ private void Control_Scroll(object sender, ScrollEventArgs e)

private void Control_LostFocus(object sender, EventArgs e)
{
#if __MonoCS__
// Mono raises TextBox.LostFocus before the clicked dropdown child
// receives its mouse event. Keep the pending click alive.
if (Host.Visible && (Host.ContainsFocus || Host.Bounds.Contains(Control.MousePosition)))
return;
#endif
if (!Host.Focused) Close();
}

Expand Down Expand Up @@ -657,7 +669,13 @@ private void ShowMenu()
point = TargetControlWrapper.GetPositionFromCharIndex(Fragment.Start);
point.Offset(2, TargetControlWrapper.TargetControl.Font.Height + 2);
//
#if __MonoCS__
point = GetMonoMenuPosition(point);
#endif
Host.Show(TargetControlWrapper.TargetControl, point);
#if __MonoCS__
ShowMonoSelectedReference();
#endif
if (CaptureFocus)
{
(Host.ListView as Control).Focus();
Expand All @@ -666,8 +684,65 @@ private void ShowMenu()
}
}
else
{
#if __MonoCS__
// The number of matches, and hence the popup size, can change
// on every keystroke. Clamp it again after each reflow.
var point = TargetControlWrapper.GetPositionFromCharIndex(Fragment.Start);
point.Offset(2, TargetControlWrapper.TargetControl.Font.Height + 2);
Host.Location = TargetControlWrapper.TargetControl.PointToScreen(GetMonoMenuPosition(point));
ShowMonoSelectedReference();
#endif
(Host.ListView as Control).Invalidate();
}
}
#if __MonoCS__
private void SizeMonoList()
{
var list = (AutocompleteListView)Host.ListView;
var area = Screen.FromControl(TargetControlWrapper.TargetControl).WorkingArea;
var maximumWidth = Math.Max(1, Math.Min(MaximumSize.Width, area.Width - 8));
var measuredWidth = new Size(240, 0).DpiScale().Width;
// Measure a bounded sample; measuring thousands of suggestions
// for every character would make the editor feel unresponsive.
var count = Math.Min(VisibleItems.Count, 64);
for (var i = 0; i < count; i++)
measuredWidth = Math.Max(measuredWidth,
TextRenderer.MeasureText(VisibleItems[i].MenuText ?? "", list.Font,
new Size(4000, list.ItemHeight), TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix).Width
+ LeftPadding + SystemInformation.VerticalScrollBarWidth + 8);
if (SelectedItemIndex >= count && SelectedItemIndex < VisibleItems.Count)
measuredWidth = Math.Max(measuredWidth,
TextRenderer.MeasureText(VisibleItems[SelectedItemIndex].MenuText ?? "", list.Font,
new Size(4000, list.ItemHeight), TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix).Width
+ LeftPadding + SystemInformation.VerticalScrollBarWidth + 8);
var height = Math.Min(MaximumSize.Height, Math.Max(list.ItemHeight + 1,
VisibleItems.Count * list.ItemHeight + 1));
list.Size = new Size(Math.Min(maximumWidth, measuredWidth), height);
list.AutoScrollMinSize = new Size(0, VisibleItems.Count * list.ItemHeight + 1);
}

private Point GetMonoMenuPosition(Point requestedClientPoint)
{
var control = TargetControlWrapper.TargetControl;
var caret = control.PointToScreen(requestedClientPoint);
var area = Screen.FromPoint(caret).WorkingArea;
var x = Math.Max(area.Left + 2, Math.Min(caret.X, area.Right - Host.Width - 2));
var above = caret.Y - control.Font.Height - Host.Height - 4;
var y = caret.Y + Host.Height <= area.Bottom - 2
? caret.Y
: above >= area.Top + 2 ? above : area.Bottom - Host.Height - 2;
y = Math.Max(area.Top + 2, Math.Min(y, area.Bottom - Host.Height - 2));
return control.PointToClient(new Point(x, y));
}

private void ShowMonoSelectedReference()
{
if (SelectedItemIndex < 0 || SelectedItemIndex >= VisibleItems.Count || !Host.Visible)
return;
((AutocompleteListView)Host.ListView).ShowToolTip(VisibleItems[SelectedItemIndex]);
}
#endif
private readonly ISharedViewState _sharedViewState;

private void BuildAutocompleteList(bool forced)
Expand Down Expand Up @@ -717,6 +792,9 @@ private void BuildAutocompleteList(bool forced)
SelectedItemIndex = 0;

Host.ListView.HighlightedItemIndex = -1;
#if __MonoCS__
SizeMonoList();
#endif
Host.CalcSize();
}

Expand Down Expand Up @@ -894,7 +972,12 @@ public void SelectNext(int shift)

public bool ProcessKey(char c, Keys keyModifiers)
{
#if __MonoCS__
var list = (AutocompleteListView)Host.ListView;
var page = Math.Max(1, list.ClientSize.Height / Math.Max(1, list.ItemHeight));
#else
var page = Host.Height/(Font.Height + 4);
#endif
if (keyModifiers == Keys.None)
switch ((Keys) c)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ internal class AutocompleteMenuHost : ToolStripDropDown
public AutocompleteMenuHost(AutocompleteMenu menu)
{
AutoClose = false;
#if __MonoCS__
// WinForms/Mono's autosizing can override the list's measured
// dimensions and restore a screen-wide popup.
AutoSize = false;
#else
AutoSize = true;
#endif

Margin = Padding.Empty;
Padding = Padding.Empty;
Expand Down Expand Up @@ -52,6 +58,14 @@ public IAutocompleteListView ListView
AutoSize = false,
AutoToolTip = false
};
#if __MonoCS__
// Mono's ToolStripControlHost can center a shrunken list in
// its previous (screen-sized) hosting rectangle, leaving the
// actual X11 child window completely outside the popup.
// Top-left alignment keeps native hit testing and painting
// inside the visible ToolStripDropDown on every reflow.
Host.ControlAlign = ContentAlignment.TopLeft;
#endif

(ListView as Control).MaximumSize = Menu.MaximumSize;
(ListView as Control).Size = Menu.MaximumSize;
Expand All @@ -74,6 +88,15 @@ public override RightToLeft RightToLeft
}
}

#if __MonoCS__
private bool KeepMonoPopup()
{
var target = Menu.TargetControlWrapper?.TargetControl;
return (target != null && target.ContainsFocus) ||
ContainsFocus || Bounds.Contains(Control.MousePosition);
}
#endif

protected override void OnPaintBackground(PaintEventArgs e)
{
using (var brush = new SolidBrush(listView.Colors.BackColor))
Expand All @@ -89,12 +112,18 @@ internal void CalcSize()
protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
#if __MonoCS__
if (KeepMonoPopup()) return;
#endif
if (!(ListView as Control).Focused)
Close();
}

private void ListView_LostFocus(object sender, EventArgs e)
{
#if __MonoCS__
if (KeepMonoPopup()) return;
#endif
if (!Focused)
Close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public MonoReferenceText()
ReadOnly = true;
DetectUrls = false;
BorderStyle = BorderStyle.None;
Font = new Font(FontFamily.GenericSansSerif, 10f);
BackColor = Color.White;
ForeColor = Color.Black;
Dock = DockStyle.Fill;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public string HTMLCode
#endif
public void ShowReference(Control owner, Point location)
{
#if __MonoCS__
ShowMonoReference(owner);
#else
// Keep the readable width and a bounded height for long articles.
// Once the current document has loaded, short articles give back
// unused vertical space without widening for oversized equations.
Expand All @@ -60,8 +63,78 @@ public void ShowReference(Control owner, Point location)
maximumHeight = Math.Min(preferred.Height, workingArea.Height);
SetReadingSize(new Size(Math.Min(preferred.Width, workingArea.Width), maximumHeight));
Show(owner, location);
#endif
}

#if __MonoCS__
private void ShowMonoReference(Control owner)
{
if (!owner.IsHandleCreated || !owner.Visible) return;
workingArea = Screen.FromControl(owner).WorkingArea;
var bounds = owner.RectangleToScreen(owner.ClientRectangle);
var preferred = new Size(600, 460).DpiScale();
var spaceRight = workingArea.Right - bounds.Right - 5;
var spaceLeft = bounds.Left - workingArea.Left - 5;
int width, x, y;
if (Math.Max(spaceRight, spaceLeft) >= new Size(280, 0).DpiScale().Width)
{
// Open beside the actual completion list, not at a fixed
// screen-relative offset that can push the help off-screen.
var right = spaceRight >= spaceLeft;
width = Math.Min(preferred.Width, right ? spaceRight : spaceLeft);
x = right ? bounds.Right + 4 : bounds.Left - width - 4;
maximumHeight = Math.Min(preferred.Height, workingArea.Height - 8);
y = Math.Max(workingArea.Top + 4,
Math.Min(bounds.Top, workingArea.Bottom - maximumHeight - 4));
}
else
{
// Very narrow displays: place a compact scrollable reference
// above or below the list instead of hiding it beyond an edge.
width = Math.Min(preferred.Width, workingArea.Width - 8);
x = Math.Max(workingArea.Left + 4,
Math.Min(bounds.Left, workingArea.Right - width - 4));
var below = workingArea.Bottom - bounds.Bottom - 4;
var above = bounds.Top - workingArea.Top - 4;
if (below >= above && below >= 120)
{
maximumHeight = Math.Min(preferred.Height, below);
y = bounds.Bottom + 4;
}
else if (above >= 120)
{
maximumHeight = Math.Min(preferred.Height, above);
y = bounds.Top - maximumHeight - 4;
}
else
{
// Last resort: a bounded overlay, still fully visible.
maximumHeight = Math.Min(preferred.Height, workingArea.Height - 8);
y = workingArea.Top + 4;
}
}
SetReadingSize(new Size(Math.Max(1, width), Math.Max(1, maximumHeight)));
Show(owner, owner.PointToClient(new Point(x, y)));
}

private void FitMonoReferenceHeight()
{
if (!Visible || referenceText.TextLength == 0 || maximumHeight <= 0) return;
// The reference is plain text on Mono: use the actual RichTextBox
// layout instead of Windows' HTML document dimensions.
var end = referenceText.GetPositionFromCharIndex(referenceText.TextLength - 1);
if (end.Y < 0 || end.Y >= referenceText.ClientSize.Height) return;
var chrome = Height - referenceText.ClientSize.Height;
var height = Math.Min(maximumHeight,
Math.Max(140, end.Y + referenceText.Font.Height * 2 + chrome));
if (height >= Height) return;
SetReadingSize(new Size(Width, height));
Location = new Point(
Math.Max(workingArea.Left + 2, Math.Min(Left, workingArea.Right - Width - 2)),
Math.Max(workingArea.Top + 2, Math.Min(Top, workingArea.Bottom - Height - 2)));
}
#endif

private void SetReadingSize(Size size)
{
Size = size;
Expand Down Expand Up @@ -110,6 +183,7 @@ public void setFunctionInfo(FunctionInfo functionInfo, string displayedSignature
{
#if __MonoCS__
referenceText.ShowDocument(functionInfo);
FitMonoReferenceHeight();
#else
HTMLCode = FunctionReferenceDocument.Render(functionInfo, displayedSignature);
#endif
Expand Down
1 change: 1 addition & 0 deletions Computator.NET.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
<File Path="build/mono/LegacyFixtureWriter.cs" />
<File Path="build/mono/SmokeTests/CompatibilityTests.cs" />
<File Path="build/mono/SmokeTests/Computator.NET.Mono.SmokeTests.csproj" />
<File Path="build/mono/SmokeTests/MonoAutocompleteTests.cs" />
<File Path="build/mono/SmokeTests/Program.cs" />
<File Path="build/mono/StartupProbe/Computator.NET.Mono.StartupProbe.csproj" />
<File Path="build/mono/StartupProbe/Program.cs" />
Expand Down
Loading
Loading