GDK W32: Draw snap indicators for AeroSnap

Indicator is a bare layered click-through native window,
painted completely by GDK, including animation.

This commit also isolates some of the more spam-ish debug logging
under ifdef.

This commit also changes the system metric used for maximal window
height for the snapping purposes. Turns out, SM_CYMAXTRACK is way
too large, use SM_CYVIRTUALSCREEN instead.

https://bugzilla.gnome.org/show_bug.cgi?id=763013
This commit is contained in:
Руслан Ижбулатов 2016-03-12 16:26:19 +00:00
parent b420c53773
commit b0131616b2
2 changed files with 545 additions and 15 deletions

View File

@ -97,6 +97,34 @@ typedef struct _AeroSnapEdgeRegion AeroSnapEdgeRegion;
*/
#define AEROSNAP_REGION_TRIGGER_THICKNESS (1)
/* The gap between the snap indicator and the edge of the work area
* (in pixels).
*/
#define AEROSNAP_INDICATOR_EDGE_GAP (10)
/* Width of the outline of the snap indicator
* (in pixels).
*/
#define AEROSNAP_INDICATOR_LINE_WIDTH (3.0)
/* Corner radius of the snap indicator.
*/
#define AEROSNAP_INDICATOR_CORNER_RADIUS (3.0)
/* The time it takes for snap indicator to expand/shrink
* from current window size to future position of the
* snapped window (in microseconds).
*/
#define AEROSNAP_INDICATOR_ANIMATION_DURATION (200 * 1000)
/* Opacity if the snap indicator. */
#define AEROSNAP_INDICATOR_OPACITY (0.5)
/* The interval between snap indicator redraws (in milliseconds).
* 16 is ~ 1/60 of a second, for ~60 FPS.
*/
#define AEROSNAP_INDICATOR_ANIMATION_TICK (16)
static gboolean _gdk_window_get_functions (GdkWindow *window,
GdkWMFunction *functions);
static HDC _gdk_win32_impl_acquire_dc (GdkWindowImplWin32 *impl);
@ -2952,6 +2980,7 @@ _gdk_window_get_functions (GdkWindow *window,
return (functions_set != NULL);
}
#if defined(MORE_AEROSNAP_DEBUGGING)
static void
log_region (gchar *prefix, AeroSnapEdgeRegion *region)
{
@ -2968,6 +2997,7 @@ log_region (gchar *prefix, AeroSnapEdgeRegion *region)
region->trigger.x,
region->trigger.y));
}
#endif
static void
calculate_aerosnap_regions (GdkW32DragMoveResizeContext *context)
@ -2975,6 +3005,9 @@ calculate_aerosnap_regions (GdkW32DragMoveResizeContext *context)
GdkDisplay *display;
GdkScreen *screen;
gint n_monitors, monitor, other_monitor;
#if defined(MORE_AEROSNAP_DEBUGGING)
gint i;
#endif
display = gdk_display_get_default ();
screen = gdk_display_get_default_screen (display);
@ -3110,8 +3143,7 @@ calculate_aerosnap_regions (GdkW32DragMoveResizeContext *context)
#undef _M_LEFT
#undef _M_RIGHT
gint i;
#if defined(MORE_AEROSNAP_DEBUGGING)
for (i = 0; i < context->maximize_regions->len; i++)
log_region ("maximize", &g_array_index (context->maximize_regions, AeroSnapEdgeRegion, i));
@ -3123,6 +3155,7 @@ calculate_aerosnap_regions (GdkW32DragMoveResizeContext *context)
for (i = 0; i < context->fullup_regions->len; i++)
log_region ("fullup", &g_array_index (context->fullup_regions, AeroSnapEdgeRegion, i));
#endif
}
static void
@ -3305,7 +3338,7 @@ snap_up (GdkWindow *window,
impl->snap_state = GDK_WIN32_AEROSNAP_STATE_FULLUP;
maxysize = GetSystemMetrics (SM_CYMAXTRACK);
maxysize = GetSystemMetrics (SM_CYVIRTUALSCREEN);
gdk_window_get_position (window, &x, &y);
width = gdk_window_get_width (window);
@ -3495,27 +3528,440 @@ apply_snap (GdkWindow *window,
}
}
static gboolean
ensure_snap_indicator_exists (GdkW32DragMoveResizeContext *context)
{
if (context->shape_indicator == NULL)
{
HWND handle;
ATOM klass;
klass = RegisterGdkClass (GDK_WINDOW_TOPLEVEL, GDK_WINDOW_TYPE_HINT_SPLASHSCREEN);
handle = CreateWindowExW (WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_NOACTIVATE,
MAKEINTRESOURCEW (klass),
L"",
WS_POPUP,
0,
0,
0, 0,
NULL,
NULL,
_gdk_app_hmodule,
NULL);
context->shape_indicator = handle;
}
return context->shape_indicator != NULL;
}
static gboolean
ensure_snap_indicator_surface (GdkW32DragMoveResizeContext *context,
gint width,
gint height)
{
if (context->indicator_surface != NULL &&
(context->indicator_surface_width < width ||
context->indicator_surface_height < height))
{
cairo_surface_destroy (context->indicator_surface);
context->indicator_surface = NULL;
}
if (context->indicator_surface == NULL)
context->indicator_surface = cairo_win32_surface_create_with_dib (CAIRO_FORMAT_ARGB32, width, height);
if (cairo_surface_status (context->indicator_surface) != CAIRO_STATUS_SUCCESS)
{
cairo_surface_destroy (context->indicator_surface);
context->indicator_surface = NULL;
return FALSE;
}
return TRUE;
}
/* Indicator is drawn with some inward offset, so that it does
* not hug screen edges.
*/
static void
adjust_indicator_rectangle (GdkRectangle *rect,
gboolean inward)
{
gdouble inverter;
gint gap;
#if defined(MORE_AEROSNAP_DEBUGGING)
GdkRectangle cache = *rect;
#endif
if (inward)
inverter = 1.0;
else
inverter = -1.0;
/* TODO: Adjust for HiDPI? */
gap = AEROSNAP_INDICATOR_EDGE_GAP;
rect->x += gap * inverter;
rect->y += gap * inverter;
rect->width -= gap * 2 * inverter;
rect->height -= gap * 2 * inverter;
#if defined(MORE_AEROSNAP_DEBUGGING)
GDK_NOTE (MISC, g_print ("Adjusted %d x %d @ %d : %d -> %d x %d @ %d : %d\n",
cache.width, cache.height, cache.x, cache.y,
rect->width, rect->height, rect->x, rect->y));
#endif
}
static void
rounded_rectangle (cairo_t *cr,
gint x,
gint y,
gint width,
gint height,
gdouble radius,
gdouble line_width,
GdkRGBA *fill,
GdkRGBA *outline)
{
gdouble degrees = M_PI / 180.0;
if (fill == NULL && outline == NULL)
return;
cairo_save (cr);
cairo_new_sub_path (cr);
cairo_arc (cr, x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
cairo_arc (cr, x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
cairo_arc (cr, x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
cairo_arc (cr, x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
cairo_close_path (cr);
if (fill)
{
cairo_set_source_rgba (cr, fill->red, fill->green, fill->blue, fill->alpha);
if (outline)
cairo_fill_preserve (cr);
else
cairo_fill (cr);
}
if (outline)
{
cairo_set_source_rgba (cr, outline->red, outline->green, outline->blue, outline->alpha);
cairo_set_line_width (cr, line_width);
cairo_stroke (cr);
}
cairo_restore (cr);
}
/* Translates linear animation scale into some kind of curve */
static gdouble
curve (gdouble val)
{
/* TODO: try different curves. For now it's just linear */
return val;
}
static gboolean
draw_indicator (GdkW32DragMoveResizeContext *context,
gint64 timestamp)
{
cairo_t *cr;
GdkRGBA outline = {0, 0, 1.0, 1.0};
GdkRGBA fill = {0, 0, 1.0, 0.8};
GdkRectangle current_rect;
gint64 current_time = g_get_monotonic_time ();
gdouble animation_progress;
gboolean last_draw;
gdouble line_width;
gdouble corner_radius;
gint64 animation_duration;
/* TODO: Adjust for HiDPI? */
line_width = AEROSNAP_INDICATOR_LINE_WIDTH;
/* TODO: Adjust for HiDPI? */
corner_radius = AEROSNAP_INDICATOR_CORNER_RADIUS;
animation_duration = AEROSNAP_INDICATOR_ANIMATION_DURATION;
last_draw = FALSE;
if (timestamp == 0 &&
current_time - context->indicator_start_time > animation_duration)
{
timestamp = context->indicator_start_time + animation_duration;
last_draw = TRUE;
}
if (timestamp != 0)
current_time = timestamp;
animation_progress = (gdouble) (current_time - context->indicator_start_time) / animation_duration;
if (animation_progress > 1.0)
animation_progress = 1.0;
if (animation_progress < 0)
animation_progress = 0;
animation_progress = curve (animation_progress);
current_rect = context->indicator_start;
current_rect.x += (context->indicator_target.x - context->indicator_start.x) * animation_progress;
current_rect.y += (context->indicator_target.y - context->indicator_start.y) * animation_progress;
current_rect.width += (context->indicator_target.width - context->indicator_start.width) * animation_progress;
current_rect.height += (context->indicator_target.height - context->indicator_start.height) * animation_progress;
if (context->op == GDK_WIN32_DRAGOP_RESIZE && last_draw)
{
switch (context->edge)
{
case GDK_WINDOW_EDGE_NORTH_WEST:
current_rect.x = context->indicator_target.x + (context->indicator_target.width - current_rect.width);
current_rect.y = context->indicator_target.y + (context->indicator_target.height - current_rect.height);
break;
case GDK_WINDOW_EDGE_NORTH:
current_rect.y = context->indicator_target.y + (context->indicator_target.height - current_rect.height);
break;
case GDK_WINDOW_EDGE_WEST:
current_rect.x = context->indicator_target.x + (context->indicator_target.width - current_rect.width);
break;
case GDK_WINDOW_EDGE_SOUTH_WEST:
current_rect.x = context->indicator_target.x + (context->indicator_target.width - current_rect.width);
current_rect.y = context->indicator_target.y;
break;
case GDK_WINDOW_EDGE_NORTH_EAST:
current_rect.x = context->indicator_target.x;
current_rect.y = context->indicator_target.y + (context->indicator_target.height - current_rect.height);
break;
case GDK_WINDOW_EDGE_SOUTH_EAST:
current_rect.x = context->indicator_target.x;
current_rect.y = context->indicator_target.y;
break;
case GDK_WINDOW_EDGE_SOUTH:
current_rect.y = context->indicator_target.y;
break;
case GDK_WINDOW_EDGE_EAST:
current_rect.x = context->indicator_target.x;
break;
}
}
cr = cairo_create (context->indicator_surface);
rounded_rectangle (cr,
current_rect.x - context->indicator_window_rect.x,
current_rect.y - context->indicator_window_rect.y,
current_rect.width, current_rect.height,
corner_radius,
line_width,
&fill, &outline);
cairo_destroy (cr);
#if defined(MORE_AEROSNAP_DEBUGGING)
GDK_NOTE (MISC, g_print ("Indicator is %d x %d @ %d : %d; current time is %" G_GINT64_FORMAT "\n",
current_rect.width, current_rect.height,
current_rect.x - context->indicator_window_rect.x,
current_rect.y - context->indicator_window_rect.y,
current_time));
#endif
return last_draw;
}
static gboolean
redraw_indicator (gpointer user_data)
{
GdkW32DragMoveResizeContext *context = user_data;
POINT window_position;
SIZE window_size;
BLENDFUNCTION blender;
HDC hdc;
POINT source_point = { 0, 0 };
gboolean last_draw;
gdouble indicator_opacity;
indicator_opacity = AEROSNAP_INDICATOR_OPACITY;
if (GDK_WINDOW_DESTROYED (context->window) ||
!ensure_snap_indicator_exists (context) ||
!ensure_snap_indicator_surface (context,
context->indicator_window_rect.width,
context->indicator_window_rect.height))
{
context->timer = 0;
return G_SOURCE_REMOVE;
}
last_draw = draw_indicator (context, context->draw_timestamp);
window_position.x = context->indicator_window_rect.x - _gdk_offset_x;
window_position.y = context->indicator_window_rect.y - _gdk_offset_y;
window_size.cx = context->indicator_window_rect.width;
window_size.cy = context->indicator_window_rect.height;
blender.BlendOp = AC_SRC_OVER;
blender.BlendFlags = 0;
blender.AlphaFormat = AC_SRC_ALPHA;
blender.SourceConstantAlpha = 255 * indicator_opacity;
hdc = cairo_win32_surface_get_dc (context->indicator_surface);
API_CALL (SetWindowPos, (context->shape_indicator,
GDK_WINDOW_HWND (context->window),
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW | SWP_SHOWWINDOW | SWP_NOACTIVATE));
#if defined(MORE_AEROSNAP_DEBUGGING)
GDK_NOTE (MISC, g_print ("Indicator window position is %ld x %ld @ %ld : %ld\n",
window_size.cx, window_size.cy,
window_position.x, window_position.y));
#endif
API_CALL (UpdateLayeredWindow, (context->shape_indicator, NULL,
&window_position, &window_size,
hdc, &source_point,
0, &blender, ULW_ALPHA));
if (last_draw)
context->timer = 0;
return last_draw ? G_SOURCE_REMOVE : G_SOURCE_CONTINUE;
}
static GdkRectangle
unity_of_rects (GdkRectangle a,
GdkRectangle b)
{
GdkRectangle u = b;
if (a.x < u.x)
{
u.width += u.x - a.x;
u.x = a.x;
}
if (a.y < u.y)
{
u.height += (u.y - a.y);
u.y = a.y;
}
if (a.x + a.width > u.x + u.width)
u.width += (a.x + a.width) - (u.x + u.width);
if (a.y + a.height > u.y + u.height)
u.height += (a.y + a.height) - (u.y + u.height);
#if defined(MORE_AEROSNAP_DEBUGGING)
GDK_NOTE (MISC, g_print ("Unified 2 rects into %d x %d @ %d : %d\n",
u.width, u.height, u.x, u.y));
#endif
return u;
}
static void
start_indicator_drawing (GdkW32DragMoveResizeContext *context,
GdkRectangle from,
GdkRectangle to)
{
(void) context;
(void) from;
(void) to;
GdkRectangle to_adjusted, from_adjusted, from_or_to;
gint64 indicator_animation_tick = AEROSNAP_INDICATOR_ANIMATION_TICK;
GDK_NOTE (MISC, g_print ("Start drawing snap indicator %d x %d @ %d : %d -> %d x %d @ %d : %d\n",
from.width, from.height, from.x, from.y, to.width, to.height, to.x, to.y));
if (GDK_WINDOW_DESTROYED (context->window))
return;
if (!ensure_snap_indicator_exists (context))
return;
from_or_to = unity_of_rects (from, to);
if (!ensure_snap_indicator_surface (context, from_or_to.width, from_or_to.height))
return;
to_adjusted = to;
adjust_indicator_rectangle (&to_adjusted, TRUE);
from_adjusted = from;
adjust_indicator_rectangle (&from_adjusted, TRUE);
context->draw_timestamp = 0;
context->indicator_start = from_adjusted;
context->indicator_target = to_adjusted;
context->indicator_window_rect = from_or_to;
context->indicator_start_time = g_get_monotonic_time ();
if (context->timer)
{
g_source_remove (context->timer);
context->timer = 0;
}
context->timer = g_timeout_add_full (G_PRIORITY_DEFAULT,
indicator_animation_tick,
redraw_indicator,
context,
NULL);
}
static void
update_fullup_indicator (GdkWindow *window,
GdkW32DragMoveResizeContext *context)
{
(void) window;
(void) context;
SHORT maxysize;
GdkRectangle from, to;
GdkRectangle to_adjusted, from_adjusted, from_or_to;
GDK_NOTE (MISC, g_print ("Update fullup indicator\n"));
if (GDK_WINDOW_DESTROYED (context->window))
return;
if (context->shape_indicator == NULL)
return;
maxysize = GetSystemMetrics (SM_CYVIRTUALSCREEN);
gdk_window_get_position (window, &to.x, &to.y);
to.width = gdk_window_get_width (window);
to.height = gdk_window_get_height (window);
to.y = 0;
to.height = maxysize;
from = context->indicator_target;
if (context->timer == 0)
{
from_adjusted = from;
adjust_indicator_rectangle (&from_adjusted, FALSE);
GDK_NOTE (MISC, g_print ("Restart fullup animation from %d x %d @ %d : %d -> %d x %d @ %d x %d\n",
context->indicator_target.width, context->indicator_target.height,
context->indicator_target.x, context->indicator_target.y,
to.width, to.height, to.x, to.y));
start_indicator_drawing (context, from_adjusted, to);
return;
}
from_or_to = unity_of_rects (from, to);
to_adjusted = to;
adjust_indicator_rectangle (&to_adjusted, TRUE);
GDK_NOTE (MISC, g_print ("Retarget fullup animation %d x %d @ %d : %d -> %d x %d @ %d x %d\n",
context->indicator_target.width, context->indicator_target.height,
context->indicator_target.x, context->indicator_target.y,
to_adjusted.width, to_adjusted.height, to_adjusted.x, to_adjusted.y));
context->indicator_target = to_adjusted;
context->indicator_window_rect = from_or_to;
ensure_snap_indicator_surface (context, from_or_to.width, from_or_to.height);
}
static void
@ -3535,7 +3981,7 @@ start_indicator (GdkWindow *window,
monitor = gdk_screen_get_monitor_at_point (screen, x, y);
gdk_screen_get_monitor_workarea (screen, monitor, &workarea);
maxysize = GetSystemMetrics (SM_CYMAXTRACK);
maxysize = GetSystemMetrics (SM_CYVIRTUALSCREEN);
gdk_window_get_position (window, &start_size.x, &start_size.y);
start_size.width = gdk_window_get_width (window);
start_size.height = gdk_window_get_height (window);
@ -3565,6 +4011,7 @@ start_indicator (GdkWindow *window,
end_size.height = workarea.height;
break;
case GDK_WIN32_AEROSNAP_STATE_FULLUP:
end_size.y = 0;
end_size.height = maxysize;
break;
}
@ -3576,10 +4023,19 @@ static void
stop_indicator (GdkWindow *window,
GdkW32DragMoveResizeContext *context)
{
(void) window;
(void) context;
GDK_NOTE (MISC, g_print ("Stop drawing snap indicator\n"));
if (context->timer)
{
g_source_remove (context->timer);
context->timer = 0;
}
API_CALL (SetWindowPos, (context->shape_indicator,
SWP_NOZORDER_SPECIFIED,
0, 0, 0, 0,
SWP_NOZORDER | SWP_NOMOVE |
SWP_NOSIZE | SWP_NOREDRAW | SWP_HIDEWINDOW | SWP_NOACTIVATE));
}
static gint
@ -3610,6 +4066,23 @@ handle_aerosnap_move_resize (GdkWindow *window,
gint halfleft = 0;
gint halfright = 0;
gint fullup = 0;
gboolean fullup_edge = FALSE;
if (context->op == GDK_WIN32_DRAGOP_RESIZE)
switch (context->edge)
{
case GDK_WINDOW_EDGE_NORTH_WEST:
case GDK_WINDOW_EDGE_NORTH_EAST:
case GDK_WINDOW_EDGE_WEST:
case GDK_WINDOW_EDGE_EAST:
case GDK_WINDOW_EDGE_SOUTH_WEST:
case GDK_WINDOW_EDGE_SOUTH_EAST:
break;
case GDK_WINDOW_EDGE_SOUTH:
case GDK_WINDOW_EDGE_NORTH:
fullup_edge = TRUE;
break;
}
for (i = 0; i < context->maximize_regions->len && maximize == 0; i++)
{
@ -3635,8 +4108,10 @@ handle_aerosnap_move_resize (GdkWindow *window,
fullup = point_in_aerosnap_region (x, y, reg);
}
#if defined(MORE_AEROSNAP_DEBUGGING)
GDK_NOTE (MISC, g_print ("AeroSnap: point %d : %d - max: %d, left %d, right %d, up %d\n",
x, y, maximize, halfleft, halfright, fullup));
#endif
if (!context->revealed)
{
@ -3658,7 +4133,7 @@ handle_aerosnap_move_resize (GdkWindow *window,
context->current_snap = GDK_WIN32_AEROSNAP_STATE_HALFRIGHT;
start_indicator (window, context, x, y, context->current_snap);
}
else if (context->op == GDK_WIN32_DRAGOP_RESIZE && fullup == 2)
else if (context->op == GDK_WIN32_DRAGOP_RESIZE && fullup == 2 && fullup_edge)
{
context->revealed = TRUE;
context->current_snap = GDK_WIN32_AEROSNAP_STATE_FULLUP;
@ -3738,7 +4213,7 @@ handle_aerosnap_move_resize (GdkWindow *window,
}
break;
case GDK_WIN32_AEROSNAP_STATE_FULLUP:
if (context->op == GDK_WIN32_DRAGOP_RESIZE && fullup > 0)
if (context->op == GDK_WIN32_DRAGOP_RESIZE && fullup > 0 && fullup_edge)
{
update_fullup_indicator (window, context);
break;
@ -4018,6 +4493,7 @@ setup_drag_move_resize_context (GdkWindow *window,
context->cursor,
timestamp);
context->window = g_object_ref (window);
context->op = op;
context->edge = edge;
context->device = device;
@ -4064,7 +4540,28 @@ gdk_win32_window_end_move_resize_drag (GdkWindow *window)
g_clear_object (&context->cursor);
context->revealed = FALSE;
g_clear_object (&context->shape_indicator);
if (context->timer)
{
g_source_remove (context->timer);
context->timer = 0;
}
g_clear_object (&context->window);
if (context->indicator_surface)
{
cairo_surface_destroy (context->indicator_surface);
context->indicator_surface = NULL;
}
if (context->shape_indicator)
{
stop_indicator (window, context);
DestroyWindow (context->shape_indicator);
context->shape_indicator = NULL;
}
g_clear_pointer (&context->halfleft_regions, g_array_unref);
g_clear_pointer (&context->halfright_regions, g_array_unref);
g_clear_pointer (&context->maximize_regions, g_array_unref);

View File

@ -98,6 +98,9 @@ typedef enum _GdkW32WindowDragOp GdkW32WindowDragOp;
struct _GdkW32DragMoveResizeContext
{
/* The window that is being moved/resized */
GdkWindow *window;
/* The kind of drag-operation going on. */
GdkW32WindowDragOp op;
@ -141,9 +144,39 @@ struct _GdkW32DragMoveResizeContext
/* This window looks like an outline and is drawn under the window
* that is being dragged. It indicates the shape the dragged window
* will take if released at a particular point.
* Indicator window size always matches the target indicator shape,
* the the actual indicator drawn on it might not, depending on
* how much time elapsed since the animation started.
*/
HWND shape_indicator;
/* Used to draw the indicator */
cairo_surface_t *indicator_surface;
gint indicator_surface_width;
gint indicator_surface_height;
/* Size/position of shape_indicator */
GdkRectangle indicator_window_rect;
/* Indicator will animate to occupy this rectangle */
GdkRectangle indicator_target;
/* Indicator will start animating from this rectangle */
GdkRectangle indicator_start;
/* Timestamp of the animation start */
gint64 indicator_start_time;
/* Timer that drives the animation */
guint timer;
/* A special timestamp, if we want to draw not how
* the animation should look *now*, but how it should
* look at arbitrary moment of time.
* Set to 0 to tell GDK to use current time.
*/
gint64 draw_timestamp;
/* Indicates that a transformation was revealed:
*
* For drag-resize: If it's FALSE,