diff --git a/Controls/EntryExtensions.cs b/Controls/EntryExtensions.cs
new file mode 100644
index 0000000..36f1d1b
--- /dev/null
+++ b/Controls/EntryExtensions.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Maui.Controls;
+
+namespace Microsoft.Maui.Controls;
+
+///
+/// Provides attached properties for Entry controls.
+///
+public static class EntryExtensions
+{
+ ///
+ /// Attached property for SelectAllOnDoubleClick behavior.
+ /// When true, double-clicking the entry selects all text instead of just the word.
+ ///
+ public static readonly BindableProperty SelectAllOnDoubleClickProperty =
+ BindableProperty.CreateAttached(
+ "SelectAllOnDoubleClick",
+ typeof(bool),
+ typeof(EntryExtensions),
+ false);
+
+ ///
+ /// Gets the SelectAllOnDoubleClick value for the specified entry.
+ ///
+ public static bool GetSelectAllOnDoubleClick(BindableObject view)
+ {
+ return (bool)view.GetValue(SelectAllOnDoubleClickProperty);
+ }
+
+ ///
+ /// Sets the SelectAllOnDoubleClick value for the specified entry.
+ ///
+ public static void SetSelectAllOnDoubleClick(BindableObject view, bool value)
+ {
+ view.SetValue(SelectAllOnDoubleClickProperty, value);
+ }
+}
diff --git a/Handlers/EntryHandler.cs b/Handlers/EntryHandler.cs
index 64af349..9184cce 100644
--- a/Handlers/EntryHandler.cs
+++ b/Handlers/EntryHandler.cs
@@ -35,6 +35,7 @@ public partial class EntryHandler : ViewHandler
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
[nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor,
+ ["SelectAllOnDoubleClick"] = MapSelectAllOnDoubleClick,
};
public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -245,4 +246,14 @@ public partial class EntryHandler : ViewHandler
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
}
}
+
+ public static void MapSelectAllOnDoubleClick(EntryHandler handler, IEntry entry)
+ {
+ if (handler.PlatformView is null) return;
+
+ if (entry is BindableObject bindable)
+ {
+ handler.PlatformView.SelectAllOnDoubleClick = EntryExtensions.GetSelectAllOnDoubleClick(bindable);
+ }
+ }
}
diff --git a/Handlers/ProgressBarHandler.cs b/Handlers/ProgressBarHandler.cs
index b5c406c..69896ae 100644
--- a/Handlers/ProgressBarHandler.cs
+++ b/Handlers/ProgressBarHandler.cs
@@ -23,6 +23,9 @@ public partial class ProgressBarHandler : ViewHandler CommandMapper = new(ViewHandler.ViewCommandMapper)
@@ -131,4 +134,34 @@ public partial class ProgressBarHandler : ViewHandler= 0)
+ {
+ handler.PlatformView.HeightRequest = visualElement.HeightRequest;
+ }
+ }
+
+ public static void MapWidth(ProgressBarHandler handler, IProgress progress)
+ {
+ if (handler.PlatformView is null) return;
+
+ if (progress is VisualElement visualElement && visualElement.WidthRequest >= 0)
+ {
+ handler.PlatformView.WidthRequest = visualElement.WidthRequest;
+ }
+ }
+
+ public static void MapVerticalOptions(ProgressBarHandler handler, IProgress progress)
+ {
+ if (handler.PlatformView is null) return;
+
+ if (progress is Microsoft.Maui.Controls.View view)
+ {
+ handler.PlatformView.VerticalOptions = view.VerticalOptions;
+ }
+ }
}
diff --git a/LinuxApplication.cs b/LinuxApplication.cs
index 97de89b..9f2f99f 100644
--- a/LinuxApplication.cs
+++ b/LinuxApplication.cs
@@ -31,6 +31,7 @@ public class LinuxApplication : IDisposable
private static int _requestRedrawCount;
private static int _drawCount;
private static int _gtkThreadId;
+ public static int GtkThreadId => _gtkThreadId;
private static DateTime _lastCounterReset = DateTime.Now;
private static bool _isRedrawing;
private static int _loopCounter = 0;
diff --git a/Rendering/GtkSkiaSurfaceWidget.cs b/Rendering/GtkSkiaSurfaceWidget.cs
index 0f05f8f..d2b16be 100644
--- a/Rendering/GtkSkiaSurfaceWidget.cs
+++ b/Rendering/GtkSkiaSurfaceWidget.cs
@@ -250,15 +250,21 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData)
{
GtkNative.gtk_widget_grab_focus(_widget);
- var (x, y, button) = ParseButtonEvent(eventData);
- Console.WriteLine($"[GtkSkiaSurfaceWidget] ButtonPress at ({x}, {y}), button={button}");
+ var (x, y, button, eventType) = ParseButtonEvent(eventData);
+
+ // GTK event types: GDK_BUTTON_PRESS=4, GDK_2BUTTON_PRESS=5, GDK_3BUTTON_PRESS=6
+ // Only process single button press events. GTK sends 2BUTTON_PRESS and 3BUTTON_PRESS
+ // events after detecting double/triple clicks, but we handle that ourselves in SkiaEntry.
+ if (eventType != 4) // GDK_BUTTON_PRESS
+ return true;
+
PointerPressed?.Invoke(this, (x, y, button));
return true;
}
private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData)
{
- var (x, y, button) = ParseButtonEvent(eventData);
+ var (x, y, button, _) = ParseButtonEvent(eventData);
PointerReleased?.Invoke(this, (x, y, button));
return true;
}
@@ -272,7 +278,6 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
public void RaisePointerPressed(double x, double y, int button)
{
- Console.WriteLine($"[GtkSkiaSurfaceWidget] RaisePointerPressed at ({x}, {y}), button={button}");
PointerPressed?.Invoke(this, (x, y, button));
}
@@ -319,10 +324,10 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
return true;
}
- private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData)
+ private static (double x, double y, int button, int eventType) ParseButtonEvent(IntPtr eventData)
{
var evt = Marshal.PtrToStructure(eventData);
- return (evt.x, evt.y, (int)evt.button);
+ return (evt.x, evt.y, (int)evt.button, evt.type);
}
private static (double x, double y) ParseMotionEvent(IntPtr eventData)
diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs
index 50357b8..c96aad9 100644
--- a/Views/SkiaEntry.cs
+++ b/Views/SkiaEntry.cs
@@ -209,6 +209,17 @@ public class SkiaEntry : SkiaView, IInputContext
typeof(SkiaEntry),
0);
+ ///
+ /// Bindable property for SelectAllOnDoubleClick.
+ /// When true, double-clicking selects all text instead of just the word.
+ ///
+ public static readonly BindableProperty SelectAllOnDoubleClickProperty =
+ BindableProperty.Create(
+ nameof(SelectAllOnDoubleClick),
+ typeof(bool),
+ typeof(SkiaEntry),
+ false);
+
///
/// Bindable property for IsReadOnly.
///
@@ -504,6 +515,16 @@ public class SkiaEntry : SkiaView, IInputContext
set => SetValue(MaxLengthProperty, value);
}
+ ///
+ /// Gets or sets whether double-clicking selects all text instead of just the word.
+ /// Useful for URL bars and similar inputs.
+ ///
+ public bool SelectAllOnDoubleClick
+ {
+ get => (bool)GetValue(SelectAllOnDoubleClickProperty);
+ set => SetValue(SelectAllOnDoubleClickProperty, value);
+ }
+
///
/// Gets or sets whether the entry is read-only.
///
@@ -1372,13 +1393,11 @@ public class SkiaEntry : SkiaView, IInputContext
public override void OnPointerPressed(PointerEventArgs e)
{
- Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})");
if (!IsEnabled) return;
// Handle right-click context menu
if (e.Button == PointerButton.Right)
{
- Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu");
ShowContextMenu(e.X, e.Y);
return;
}
@@ -1409,15 +1428,22 @@ public class SkiaEntry : SkiaView, IInputContext
var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left + _scrollOffset;
_cursorPosition = GetCharacterIndexAtX(clickX);
- // Check for double-click (select word)
+ // Check for double-click (select word or select all)
var now = DateTime.UtcNow;
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
var distanceFromLastClick = Math.Abs(e.X - _lastClickX);
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
{
- // Double-click: select the word at cursor
- SelectWordAtCursor();
+ // Double-click: select all or select word based on property
+ if (SelectAllOnDoubleClick)
+ {
+ SelectAll();
+ }
+ else
+ {
+ SelectWordAtCursor();
+ }
_lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues
_isSelecting = false;
}
diff --git a/Views/SkiaImageButton.cs b/Views/SkiaImageButton.cs
index ccbf614..d1c7c71 100644
--- a/Views/SkiaImageButton.cs
+++ b/Views/SkiaImageButton.cs
@@ -805,12 +805,9 @@ public class SkiaImageButton : SkiaView
}
// Fill (3) and Start (0) both use y = bounds.Top
- var result1 = new Rect(x, y, finalWidth, finalHeight);
- Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (aligned): Y={result1.Y}, Height={result1.Height}");
- return result1;
+ return new Rect(x, y, finalWidth, finalHeight);
}
- Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (unchanged): Y={bounds.Y}, Height={bounds.Height}");
return bounds;
}
diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs
index 78b4e28..2b1ce69 100644
--- a/Views/SkiaLayoutView.cs
+++ b/Views/SkiaLayoutView.cs
@@ -248,13 +248,7 @@ public abstract class SkiaLayoutView : SkiaView
public override SkiaView? HitTest(float x, float y)
{
if (!IsVisible || !IsEnabled || !Bounds.Contains(x, y))
- {
- if (this is SkiaBorder)
- {
- Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={Bounds}, IsVisible={IsVisible}, IsEnabled={IsEnabled}");
- }
return null;
- }
// Hit test children in reverse order (top-most first)
for (int i = _children.Count - 1; i >= 0; i--)
@@ -262,19 +256,9 @@ public abstract class SkiaLayoutView : SkiaView
var child = _children[i];
var hit = child.HitTest(x, y);
if (hit != null)
- {
- if (this is SkiaBorder)
- {
- Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={Bounds}, child={hit.GetType().Name}");
- }
return hit;
- }
}
- if (this is SkiaBorder)
- {
- Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}");
- }
return this;
}
diff --git a/Views/SkiaProgressBar.cs b/Views/SkiaProgressBar.cs
index 850cd07..b3ce4d4 100644
--- a/Views/SkiaProgressBar.cs
+++ b/Views/SkiaProgressBar.cs
@@ -187,9 +187,22 @@ public class SkiaProgressBar : SkiaView
var barHeight = (float)BarHeight;
var cornerRadius = (float)CornerRadius;
- float midY = bounds.MidY;
- float trackTop = midY - barHeight / 2f;
- float trackBottom = midY + barHeight / 2f;
+ // If bounds height is small (HeightRequest was set), use the full bounds height
+ // Otherwise, center the bar vertically within the bounds
+ float trackTop, trackBottom;
+ if (bounds.Height <= barHeight + 4)
+ {
+ // Small bounds - fill the height
+ trackTop = bounds.Top;
+ trackBottom = bounds.Bottom;
+ }
+ else
+ {
+ // Large bounds - center the bar
+ float midY = bounds.MidY;
+ trackTop = midY - barHeight / 2f;
+ trackBottom = midY + barHeight / 2f;
+ }
// Get colors
var trackColorSK = ToSKColor(TrackColor);
@@ -246,8 +259,11 @@ public class SkiaProgressBar : SkiaView
protected override Size MeasureOverride(Size availableSize)
{
- var barHeight = BarHeight;
- return new Size(200, barHeight + 8);
+ // Respect HeightRequest if set, otherwise use BarHeight with padding
+ var height = HeightRequest >= 0 ? HeightRequest : BarHeight + 8;
+ // Respect WidthRequest if set, otherwise use available width or default
+ var width = WidthRequest >= 0 ? WidthRequest : (availableSize.Width > 0 ? availableSize.Width : 200);
+ return new Size(width, height);
}
#endregion
diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs
index ee1079c..7461fad 100644
--- a/Views/SkiaView.cs
+++ b/Views/SkiaView.cs
@@ -6,6 +6,7 @@ using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers;
+using Microsoft.Maui.Platform.Linux.Native;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
using Microsoft.Maui.Platform.Linux.Window;
@@ -1270,8 +1271,27 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible
///
/// Requests that this view be redrawn.
+ /// Thread-safe - will marshal to GTK thread if needed.
///
public void Invalidate()
+ {
+ // Check if we're on the GTK thread - if not, marshal the entire call
+ int currentThread = Environment.CurrentManagedThreadId;
+ int gtkThread = LinuxApplication.GtkThreadId;
+ if (gtkThread != 0 && currentThread != gtkThread)
+ {
+ GLibNative.IdleAdd(() =>
+ {
+ InvalidateInternal();
+ return false;
+ });
+ return;
+ }
+
+ InvalidateInternal();
+ }
+
+ private void InvalidateInternal()
{
LinuxApplication.LogInvalidate(GetType().Name);
Invalidated?.Invoke(this, EventArgs.Empty);
@@ -1286,7 +1306,7 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible
if (_parent != null)
{
- _parent.Invalidate();
+ _parent.InvalidateInternal();
}
else
{