Attempt to add cursor rendering

This commit is contained in:
wheaney 2024-03-28 13:54:53 -07:00
parent a109b5e897
commit fe828db999
5 changed files with 517 additions and 12 deletions

View File

@ -0,0 +1,68 @@
// Taken from https://github.com/jkitching/soft-brightness-plus
//
// Copyright (C) 2023 Joel Kitching (jkitching on Github)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
// Copied almost verbatim from ui/magnifier.js.
export const MouseSpriteContent = GObject.registerClass({
Implements: [Clutter.Content],
}, class MouseSpriteContent extends GObject.Object {
_init() {
super._init();
this._texture = null;
}
vfunc_get_preferred_size() {
if (!this._texture)
return [false, 0, 0];
return [true, this._texture.get_width(), this._texture.get_height()];
}
vfunc_paint_content(actor, node, _paintContext) {
if (!this._texture)
return;
let color = Clutter.Color.from_string('#ffffff'); // white
let [minFilter, magFilter] = actor.get_content_scaling_filters();
let textureNode = new Clutter.TextureNode(this._texture,
color, minFilter, magFilter);
textureNode.set_name('SoftBrightnessPlusMouseSpriteContent');
node.add_child(textureNode);
textureNode.add_rectangle(actor.get_content_box());
}
get texture() {
return this._texture;
}
set texture(coglTexture) {
if (this._texture === coglTexture)
return;
let oldTexture = this._texture;
this._texture = coglTexture;
this.invalidate();
if (!oldTexture || !coglTexture ||
oldTexture.get_width() !== coglTexture.get_width() ||
oldTexture.get_height() !== coglTexture.get_height())
this.invalidate_size();
}
});

View File

@ -0,0 +1,312 @@
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
import { MouseSpriteContent } from './cursor.js';
// Taken from https://github.com/jkitching/soft-brightness-plus
export class CursorManager {
constructor(logger, settings, mainActor) {
this._logger = logger;
this._settings = settings;
this._mainActor = mainActor;
this._enableTimeoutId = null;
this._changeHookFn = null;
this._cloneMouseSetting = null;
this._cloneMouseSettingChangedConnection = null;
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
this._cursorWantedVisible = null;
this._cursorTracker = null;
this._cursorTrackerSetPointerVisible = null;
this._cursorTrackerSetPointerVisibleBound = null;
this._cursorSprite = null;
this._cursorActor = null;
this._cursorWatcher = null;
this._cursorSeat = null;
// Set/destroyed by _startCloningMouse / _stopCloningMouse
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
// Set/destroyed by _delayedSetPointerInvisible/_clearDelayedSetPointerInvibleCallbacks
this._delayedSetPointerInvisibleIdleSource = null;
}
setChangeHook(fn) {
this._changeHookFn = fn;
}
enable() {
// First 500ms: For some reason, starting the mouse cloning at this
// stage fails when gnome-shell is restarting on x11 and the mouse
// listener doesn't receive any events. Adding a small delay before
// starting the whole mouse cloning business helps.
this._enableTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
// Wait 500ms before starting to check for the _brightness object.
this._enableTimeoutId = null;
this._enable();
// Ensure proper stacking order for cursor and overlay.
if (this._changeHookFn !== null) {
this._changeHookFn();
}
return GLib.SOURCE_REMOVE;
});
}
_enable() {
this._cloneMouseSetting = true; // this._settings.get_boolean('clone-mouse');
this._enableCloningMouse();
// this._cloneMouseSettingChangedConnection = this._settings.connect('changed::clone-mouse', this._on_clone_mouse_change.bind(this));
}
disable() {
// If _enableTimeoutId is non-null, _enable() has not run yet, and will
// not run. Do not run _disable() in this case.
GLib.source_remove(this._enableTimeoutId);
if (this._enableTimeoutId !== null) {
return;
}
this._enableTimeoutId = null;
this._changeHookFn = null;
// this._settings.disconnect(this._cloneMouseSettingChangedConnection);
// this._cloneMouseSettingChangedConnection = null;
this._disableCloningMouse();
this._cloneMouseSetting = null;
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
this._cursorWantedVisible = null;
this._cursorTracker = null;
this._cursorTrackerSetPointerVisible = null;
this._cursorTrackerSetPointerVisibleBound = null;
this._cursorSprite = null;
this._cursorActor = null;
this._cursorWatcher = null;
this._cursorSeat = null;
// Set/destroyed by _startCloningMouse / _stopCloningMouse
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
// Set/destroyed by _delayedSetPointerInvisible/_clearDelayedSetPointerInvibleCallbacks
this._delayedSetPointerInvisibleIdleSource = null;
}
startCloning() {
if (this._cursorWantedVisible) {
this._startCloningMouse();
}
}
stopCloning() {
this._stopCloningShowMouse();
}
hidePointer() {
this._setPointerVisible(false);
}
_isMouseClonable() {
return this._cloneMouseSetting;
}
_on_clone_mouse_change() {
const cloneMouse = true; // this._settings.get_boolean('clone-mouse');
if (cloneMouse == this._cloneMouseSetting) {
this._logger.log_debug('_on_clone_mouse_change(): no setting change, no change');
return;
}
if (cloneMouse) {
// Starting to clone mouse
this._logger.log_debug('_on_clone_mouse_change(): starting mouse cloning');
this._cloneMouseSetting = true;
this._enableCloningMouse();
if (this._changeHookFn !== null) {
this._changeHookFn();
}
} else {
this._logger.log_debug('_on_clone_mouse_change(): stopping mouse cloning');
this._disableCloningMouse();
this._cloneMouseSetting = false;
}
}
_enableCloningMouse() {
if (!this._isMouseClonable()) {
return;
}
this._logger.log_debug('_enableCloningMouse()');
this._cursorWantedVisible = true;
this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
this._cursorTrackerSetPointerVisible = Meta.CursorTracker.prototype.set_pointer_visible;
this._cursorTrackerSetPointerVisibleBound = this._cursorTrackerSetPointerVisible.bind(this._cursorTracker);
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisibleReplacement.bind(this);
this._cursorSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
this._cursorSprite.content = new MouseSpriteContent();
this._cursorActor = new Clutter.Actor();
this._cursorActor.add_actor(this._cursorSprite);
this._cursorWatcher = PointerWatcher.getPointerWatcher();
this._cursorSeat = Clutter.get_default_backend().get_default_seat();
}
_disableCloningMouse() {
if (!this._isMouseClonable()) {
return;
}
this._stopCloningShowMouse();
this._logger.log_debug('_disableCloningMouse()');
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
this._cursorWantedVisible = null;
this._cursorTracker = null;
this._cursorTrackerSetPointerVisible = null;
this._cursorTrackerSetPointerVisibleBound = null;
this._cursorSprite = null;
this._cursorActor = null;
this._cursorWatcher = null;
this._cursorSeat = null;
}
_setPointerVisible(visible) {
if (!this._isMouseClonable()) {
return;
}
this._cursorTrackerSetPointerVisibleBound(visible);
}
_cursorTrackerSetPointerVisibleReplacement(visible) {
if (visible) {
this._startCloningMouse();
// For some reason, exiting the magnifier causes the
// stacking order for the cursor and overlay actors to be
// swapped around. Reassert stacking order whenever the
// pointer should become visible again.
if (this._changeHookFn !== null) {
this._changeHookFn();
}
} else {
this._stopCloningMouse();
this._setPointerVisible(false);
}
this._cursorWantedVisible = visible;
}
_startCloningMouse() {
if (!this._isMouseClonable()) {
return;
}
this._logger.log_debug('_startCloningMouse()');
if (this._cursorWatch == null) {
this._mainActor.add_actor(this._cursorActor);
this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
const interval = 1000 / 60;
this._logger.log_debug('_startCloningMouse(): watch interval = ' + interval + ' ms');
this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
this._updateMouseSprite();
this._updateMousePosition();
}
this._setPointerVisible(false);
if (this._cursorTracker.set_keep_focus_while_hidden) {
this._cursorTracker.set_keep_focus_while_hidden(true);
}
if (!this._cursorSeat.is_unfocus_inhibited()) {
this._cursorSeat.inhibit_unfocus();
}
}
_stopCloningShowMouse() {
if (!this._isMouseClonable()) {
return;
}
this._logger.log_debug('_stopCloningShowMouse(), restoring cursor visibility to ' + this._cursorWantedVisible);
this._stopCloningMouse();
this._setPointerVisible(this._cursorWantedVisible);
if (this._cursorTracker.set_keep_focus_while_hidden) {
this._cursorTracker.set_keep_focus_while_hidden(false);
}
if (this._cursorSeat.is_unfocus_inhibited()) {
this._cursorSeat.uninhibit_unfocus();
}
}
_stopCloningMouse() {
if (!this._isMouseClonable()) {
return;
}
if (this._cursorWatch != null) {
this._logger.log_debug('_stopCloningMouse()');
this._cursorWatch.remove();
this._cursorWatch = null;
this._cursorTracker.disconnect(this._cursorChangedConnection);
this._cursorChangedConnection = null;
this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
this._cursorVisibilityChangedConnection = null;
this._mainActor.remove_actor(this._cursorActor);
}
this._clearDelayedSetPointerInvibleCallbacks();
}
_updateMousePosition(actor, event) {
const [x, y, mask] = global.get_pointer();
this._cursorActor.set_position(x, y);
this._delayedSetPointerInvisible();
}
_updateMouseSprite() {
const sprite = this._cursorTracker.get_sprite();
if (sprite) {
this._cursorSprite.content.texture = sprite;
this._cursorSprite.show();
} else {
this._cursorSprite.hide();
}
const [xHot, yHot] = this._cursorTracker.get_hot();
this._cursorSprite.set({
translation_x: -xHot,
translation_y: -yHot,
});
this._delayedSetPointerInvisible();
}
_delayedSetPointerInvisible() {
this._setPointerVisible(false);
// Clear the pointer upon entering idle loop
if (this._delayedSetPointerInvisibleIdleSource == null) {
this._delayedSetPointerInvisibleIdleSource = GLib.idle_add(
GLib.PRIORITY_DEFAULT,
() => {
this._setPointerVisible(false);
this._delayedSetPointerInvisibleIdleSource = null;
return false;
}
);
}
}
_clearDelayedSetPointerInvibleCallbacks() {
if (this._delayedSetPointerInvisibleIdleSource != null) {
GLib.source_remove(this._delayedSetPointerInvisibleIdleSource);
this._delayedSetPointerInvisibleIdleSource = null;
}
}
}

View File

@ -4,9 +4,12 @@ import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import Meta from 'gi://Meta';
import ExtensionUtils from 'gi://ExtensionUtils';
import Main from 'gi://Main';
import PanelMenu from 'gi://PanelMenu';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Logger from './logger.js';
import { CursorManager } from './cursormanager.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
const UINT8_SIZE = 1;
const BOOL_SIZE = UINT8_SIZE;
@ -195,12 +198,32 @@ function setIntermittentUniformVariables() {
}
export default class ExampleExtension extends Extension {
export default class BreezyDesktopExtension extends Extension {
constructor(metadata, uuid) {
super(metadata, uuid);
this._extensionPath = metadata.path;
// Set/destroyed by enable/disable
this._settings = null;
this._logger = null;
this._cursorManager = null;
this._removeSettingsCallbacks = [];
}
enable() {
// this._settings = this.getSettings();
this._logger = new Logger.Logger('soft-brightness-plus', this.metadata, Config.PACKAGE_VERSION);
// this._logger.set_debug(this._settings.get_boolean('debug'));
this._logger.log_debug('enable(), session mode = ' + Main.sessionMode.currentMode);
this._logger.logVersion();
this._cursorManager = new CursorManager(this._logger, this._settings, global.stage);
this._cursorManager.enable();
const extensionPath = this._extensionPath;
var XREffect = GObject.registerClass({}, class XREffect extends Shell.GLSLEffect {
vfunc_build_pipeline() {
const shaderPath = GLib.getenv('BREEZY_GNOME_SHADER_PATH');
const code = getShaderSource(shaderPath);
const code = getShaderSource(`${extensionPath}/IMUAdjust.frag`);
const main = 'PS_IMU_Transform(vec4(0, 0, 0, 0), cogl_tex_coord_in[0].xy, cogl_color_out);';
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false);
@ -216,7 +239,6 @@ export default class ExampleExtension extends Extension {
if (data[0]) {
const buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer);
var repaintNeeded = false;
if (!this._initialized) {
this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
@ -227,8 +249,9 @@ export default class ExampleExtension extends Extension {
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
this.setIntermittentUniformVariables();
this._repaint_needed = false;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
repaintNeeded = true;
this._repaint_needed = true;
this.queue_repaint();
return GLib.SOURCE_CONTINUE;
});
@ -243,9 +266,10 @@ export default class ExampleExtension extends Extension {
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
// if (repaintNeeded) {
if (this._repaint_needed) {
super.vfunc_paint_target(node, paintContext);
// }
this._repaint_needed = false;
}
}
}
});
@ -254,6 +278,9 @@ export default class ExampleExtension extends Extension {
}
disable() {
this._logger.log_debug('disable(), session mode = ' + Main.sessionMode.currentMode);
this._cursorManager.disable();
this._cursorManager = null;
}
}

View File

@ -0,0 +1,98 @@
// Taken from https://github.com/jkitching/soft-brightness-plus
//
// Copyright (C) 2023 Joel Kitching (jkitching on Github)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import GLib from 'gi://GLib';
import System from 'system';
export class Logger {
constructor(title, metadata, packageVersion) {
this._title = title;
this._metadata = metadata;
this._packageVersion = packageVersion;
this._first_log = true;
this._debug = false;
}
get_version() {
return this._metadata['version'] + ' / git ' + this._metadata['vcs_revision'];
}
logVersion() {
const gnomeShellVersion = Config.PACKAGE_VERSION;
if (gnomeShellVersion != undefined) {
const splitVersion = gnomeShellVersion.split('.').map((x) => {
x = Number(x);
if (Number.isNaN(x)) {
return 0;
} else {
return x;
}
});
const major = splitVersion[0];
const minor = splitVersion.length >= 2 ? splitVersion[1] : 0;
const patch = splitVersion.length >= 3 ? splitVersion[2] : 0;
const xdgSessionType = GLib.getenv('XDG_SESSION_TYPE');
const onWayland = xdgSessionType == 'wayland';
this.log_debug('_logVersion(): gnome-shell version major=' + major + ', minor=' + minor + ', patch=' + patch + ', system_version=' + System.version + ', XDG_SESSION_TYPE=' + xdgSessionType);
this.log_debug('_logVersion(): onWayland=' + onWayland);
}
}
log(text) {
if (this._first_log) {
this._first_log = false;
let msg = 'version ' + this.get_version();
const gnomeShellVersion = this._packageVersion;
if (gnomeShellVersion != undefined) {
msg += ' on Gnome-Shell ' + gnomeShellVersion;
}
const gjsVersion = System.version;
if (gjsVersion != undefined) {
const gjsVersionMajor = Math.floor(gjsVersion / 10000);
const gjsVersionMinor = Math.floor((gjsVersion % 10000) / 100);
const gjsVersionPatch = gjsVersion % 100;
msg += (' / gjs ' + gjsVersionMajor +
'.' + gjsVersionMinor +
'.' + gjsVersionPatch +
' (' + gjsVersion + ')'
);
}
const sessionType = GLib.getenv('XDG_SESSION_TYPE');
if (sessionType != undefined) {
msg += ' / ' + sessionType;
}
this.log(msg);
}
console.log('' + this._title + ': ' + text);
}
log_debug(text) {
if (this._debug) {
this.log(text);
}
}
set_debug(debug) {
this._debug = debug;
}
get_debug() {
return this._debug;
}
};

View File

@ -1,7 +1,7 @@
{
"uuid": "breezydesktop@org.xronlinux",
"name": "Breezy GNOME",
"description": "XR virtual desktop for Linux.",
"name": "Breezy GNOME XR Desktop",
"description": "XR virtual desktop for GNOME.",
"shell-version": [
"45", "46"
],