From 3ede7c779d8c740eac11c297e6a4ea45e80f2d1b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 31 Mar 2020 15:38:21 -0600 Subject: [PATCH] feat: Window movements are now animated To present a smooth first class experience --- src/extension.ts | 38 +++++++++++++++--------- src/forest.ts | 21 ++++++------- src/mod.d.ts | 5 ++++ src/tiling.ts | 40 +++++++++++++++---------- src/tweener.ts | 41 ++++++++++++++++++++++++++ src/window.ts | 77 +++++++++++++++++++++++++++++++++++++----------- 6 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 src/tweener.ts diff --git a/src/extension.ts b/src/extension.ts index 0040e5a7..61358e5f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -72,6 +72,8 @@ export class Ext extends Ecs.World { // State + /** Animate window movements */ + animate_windows: boolean = true; /** Column sizes in snap-to-grid */ column_size: number = 128; @@ -205,7 +207,6 @@ export class Ext extends Ecs.World { } else { this.signals.set(object, [signal]); } - } connect_meta(win: Window.ShellWindow, signal: string, callback: () => void): number { @@ -561,24 +562,30 @@ export class Ext extends Ecs.World { /** Handle window creation events */ on_window_create(window: Meta.Window) { GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { - let win = this.get_window(window); let actor = window.get_compositor_private(); - if (win && actor) { - const entity = win.entity; - actor.connect('destroy', () => { - if (win) this.on_destroy(entity); - return false; - }); - - if (win.is_tilable(this)) { - this.connect_window(win); - } + if (actor) { + this.on_window_create_inner(window, actor); } return false; }); } + on_window_create_inner(window: Meta.Window, actor: Clutter.Actor) { + let win = this.get_window(window); + if (win) { + const entity = win.entity; + actor.connect('destroy', () => { + if (win) this.on_destroy(entity); + return false; + }); + + if (win.is_tilable(this)) { + this.connect_window(win); + } + } + } + /** Handle workspace change events */ on_workspace_changed(win: Window.ShellWindow) { Log.debug(`workspace changed for ${win.name(this)}`); @@ -948,6 +955,9 @@ export class Ext extends Ecs.World { // If not found, create a new entity with a ShellWindow component. if (!entity) { + const actor = meta.get_compositor_private(); + if (!actor) return null; + let window_app: any, name: string; try { @@ -967,8 +977,8 @@ export class Ext extends Ecs.World { this.monitors.insert(entity, [win.meta.get_monitor(), win.workspace_id()]); Log.debug(`created window (${win.entity}): ${win.name(this)}: ${id}`); - const actor = meta.get_compositor_private(); - if (this.auto_tiler && win.is_tilable(this) && actor) { + + if (this.auto_tiler && win.is_tilable(this)) { let id = actor.connect('first-frame', () => { this.auto_tiler?.auto_tile(this, win, this.init); actor.disconnect(id); diff --git a/src/forest.ts b/src/forest.ts index ea26dbf5..f3f0ffb8 100644 --- a/src/forest.ts +++ b/src/forest.ts @@ -80,7 +80,7 @@ export class Forest extends Ecs.World { ? ext.workspace_by_id(workspace) : null; - const new_positions = new Array(); + // const new_positions = new Array(); for (const [entity, r] of this.requested) { const window = ext.windows.get(entity); if (!window) continue; @@ -95,12 +95,11 @@ export class Forest extends Ecs.World { const signals = ext.size_signals.get(window.entity); if (signals) { Log.debug(`Moving Window(${entity}) from [${backup.fmt()}] to [${r.rect.fmt()}]`); - move_window(window, r.rect, signals); - - const actual = window.rect(); - Log.debug(`Moved Window(${entity}) to ${actual.fmt()}`); - - new_positions.push([window, backup, actual]); + move_window(ext, window, r.rect, signals, () => { + const actual = window.rect(); + Log.debug(`Moved Window(${entity}) to ${actual.fmt()}`); + // new_positions.push([window, backup, actual]); + }); } else { Log.error(`Attempted move of Window(${entity}), but it does not have attached signals`); } @@ -704,7 +703,7 @@ export class Forest extends Ecs.World { } -function move_window(window: ShellWindow, rect: Rectangular, signals: [SignalID, SignalID, SignalID]) { +function move_window(ext: Ext, window: ShellWindow, rect: Rectangular, signals: [SignalID, SignalID, SignalID], on_complete: () => void) { if (!(window.meta instanceof Meta.Window)) { Log.error(`attempting to a window entity in a tree which lacks a Meta.Window`); return; @@ -718,6 +717,8 @@ function move_window(window: ShellWindow, rect: Rectangular, signals: [SignalID, } for (const sig of signals) utils.block_signal(window.meta, sig); - window.move(rect); - for (const sig of signals) utils.unblock_signal(window.meta, sig); + window.move(ext, rect, () => { + for (const sig of signals) utils.unblock_signal(window.meta, sig); + on_complete(); + }); } diff --git a/src/mod.d.ts b/src/mod.d.ts index f620a1ba..d1b081a8 100644 --- a/src/mod.d.ts +++ b/src/mod.d.ts @@ -76,14 +76,18 @@ declare namespace Clutter { add(child: Actor): void; destroy(): void; + ease(params: Object): void; hide(): void; get_child_at_index(nth: number): Clutter.Actor | null; get_n_children(): number; get_parent(): Clutter.Actor | null; + get_transition(param: string): any | null; is_visible(): boolean; remove_all_children(): void; + remove_all_transitions(): void; remove_child(child: Actor): void; set_child_below_sibling(child: Actor, sibling: Actor | null): void; + set_easing_duration(msecs: number | null): void; show(): void; } @@ -113,6 +117,7 @@ declare namespace Meta { activate(time: number): void; change_workspace_by_index(workspace: number, append: boolean): void; + get_buffer_rect(): Rectangular; get_compositor_private(): Clutter.Actor | null; get_description(): string; get_frame_rect(): Rectangular; diff --git a/src/tiling.ts b/src/tiling.ts index 718ef251..84c0b05e 100644 --- a/src/tiling.ts +++ b/src/tiling.ts @@ -8,13 +8,13 @@ import * as GrabOp from 'grab_op'; import * as Rect from 'rectangle'; import * as window from 'window'; import * as shell from 'shell'; +import * as Tweener from 'tweener'; import type { Entity } from './ecs'; import type { Rectangle } from './rectangle'; import type { Ext } from './extension'; import { AutoTiler } from './auto_tiler'; -const GLib: GLib = imports.gi.GLib; const { Meta } = imports.gi; const Main = imports.ui.main; const { ShellWindow } = window; @@ -233,9 +233,9 @@ export class Tiler { ext.auto_tiler.forest.arrange(ext, fork.workspace); - GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + let actor = window.meta.get_compositor_private(); + if (actor) Tweener.on_tween_completion(actor, () => { ext.set_overlay(window.rect()); - return false; }); } } @@ -308,6 +308,8 @@ export class Tiler { if (move_to === null) return; const focused = ext.focus_window(); + let watching: null | window.ShellWindow = null; + if (ext.auto_tiler && focused) { if (move_to instanceof ShellWindow) { const parent = ext.auto_tiler.windows_are_siblings(focused.entity, move_to.entity); @@ -318,24 +320,32 @@ export class Tiler { fork.left.entity = (fork.right as any).entity; (fork.right as any).entity = temp; ext.auto_tiler.tile(ext, fork, fork.area as any); - ext.set_overlay(focused.rect()); - focused.activate(); - return; + watching = focused } } - ext.auto_tiler.detach_window(ext, focused.entity); - ext.auto_tiler.attach_to_window(ext, move_to, focused, Lib.cursor_rect()); - ext.set_overlay(focused.rect()); - focused.activate(); + if (!watching) { + ext.auto_tiler.detach_window(ext, focused.entity); + ext.auto_tiler.attach_to_window(ext, move_to, focused, Lib.cursor_rect()); + watching = focused; + } } else { global.log(`attach to monitor ${move_to}`); ext.auto_tiler.detach_window(ext, focused.entity); ext.auto_tiler.attach_to_monitor(ext, focused, [move_to, ext.active_workspace()]); - ext.set_overlay(focused.rect()); - focused.activate(); + watching = focused; } } + + if (watching) { + let actor = watching.meta.get_compositor_private(); + if (actor) Tweener.on_tween_completion(actor, () => { + if (watching) { + ext.set_overlay(watching.rect()); + watching.activate(); + } + }); + } } move_left(ext: Ext) { @@ -483,12 +493,12 @@ export class Tiler { if (ext.auto_tiler) { ext.auto_tiler.attach_swap(this.swap_window, this.window); } - meta_swap.move(meta.rect()); + meta_swap.move(ext, meta.rect()); this.swap_window = null; } } - meta.move(ext.overlay); + meta.move(ext, ext.overlay); ext.add_tag(this.window, Tags.Tiled); } } @@ -523,7 +533,7 @@ export class Tiler { 0, 0, 0, 0 ); - win.move(rect); + win.move(ext, rect); ext.snapped.insert(win.entity, true); } diff --git a/src/tweener.ts b/src/tweener.ts new file mode 100644 index 00000000..92e515c8 --- /dev/null +++ b/src/tweener.ts @@ -0,0 +1,41 @@ +const GLib: GLib = imports.gi.GLib; +const { Clutter } = imports.gi; + +export interface TweenParams { + x: number; + y: number; + width: number; + height: number; + duration: number; + mode: any | null; + onComplete: () => void; +} + +export function add(a: Clutter.Actor, p: TweenParams) { + if (!p.mode) p.mode = Clutter.AnimationMode.LINEAR; + + a.ease(p); +} + +export function remove(a: Clutter.Actor) { + a.remove_all_transitions(); +} + +export function is_tweening(a: Clutter.Actor) { + return a.get_transition('x') + || a.get_transition('y') + || a.get_transition('width') + || a.get_transition('height') + || a.get_transition('scale-x') + || a.get_transition('scale-x'); +} + +export function on_tween_completion(actor: Clutter.Actor, callback: () => void) { + GLib.timeout_add(150, GLib.PRIORITY_DEFAULT, () => { + if (is_tweening(actor)) return true; + + callback(); + + return false; + }); +} \ No newline at end of file diff --git a/src/window.ts b/src/window.ts index 2dab14ed..b9ee4309 100644 --- a/src/window.ts +++ b/src/window.ts @@ -1,6 +1,7 @@ // @ts-ignore const Me = imports.misc.extensionUtils.getCurrentExtension(); +import * as Ecs from 'ecs'; import * as lib from 'lib'; import * as log from 'log'; import * as once_cell from 'once_cell'; @@ -8,11 +9,13 @@ import * as Rect from 'rectangle'; import * as Tags from 'tags'; import * as utils from 'utils'; import * as xprop from 'xprop'; +import * as Tweener from 'tweener'; import type { Entity } from './ecs'; import type { Ext } from './extension'; import type { Rectangle } from './rectangle'; +// const GLib: GLib = imports.gi.GLib; const { Gdk, Meta, Shell, St } = imports.gi; const { OnceCell } = once_cell; @@ -149,18 +152,58 @@ export class ShellWindow { return this.meta.get_transient_for() !== null; } - move(rect: Rectangular) { - this.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL); - this.meta.unmaximize(Meta.MaximizeFlags.VERTICAL); - this.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL); + move(ext: Ext, rect: Rectangular, on_complete: () => void = () => { }) { + let clone = Rect.Rectangle.from_meta(rect); + let actor = this.meta.get_compositor_private(); + if (actor) { + this.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL); + this.meta.unmaximize(Meta.MaximizeFlags.VERTICAL); + this.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL); - this.meta.move_resize_frame( - true, - rect.x, - rect.y, - rect.width, - rect.height - ); + if (ext.animate_windows) { + let current = this.meta.get_frame_rect(); + let buffer = this.meta.get_buffer_rect(); + + let dx = current.x - buffer.x; + let dy = current.y - buffer.y; + + if (ext.active_hint && ext.active_hint.window && Ecs.entity_eq(ext.active_hint.window.entity, this.entity)) { + ext.active_hint.hide(); + } + + if (Tweener.is_tweening(actor)) Tweener.remove(actor); + + Tweener.add(actor, { + x: clone.x - dx, + y: clone.y - dy, + width: clone.width, + height: clone.height, + duration: 150, + mode: null, + onComplete: () => { + this.meta.move_resize_frame( + true, + clone.x, + clone.y, + clone.width, + clone.height + ); + + on_complete(); + } + }); + } else { + this.meta.move_resize_frame( + true, + clone.x, + clone.y, + clone.width, + clone.height + ); + + on_complete(); + } + } } name(ext: Ext): string { @@ -178,14 +221,12 @@ export class ShellWindow { }); } - swap(other: ShellWindow): void { - let ar = this.rect(); - let br = other.rect(); - - this.move(br); - other.move(ar); + swap(ext: Ext, other: ShellWindow): void { + let ar = this.rect().clone(); + let br = other.rect().clone(); - place_pointer_on(this.meta); + other.move(ext, ar); + this.move(ext, br, () => place_pointer_on(this.meta)); } wm_role(): string | null {