You're in K rove's space

When Masking GUI Elements

In your quest to bend every piece of software to your will, you're faced with the challenge of shrouding extraneous stuff in your sight. Will you succeed, or will you accept defeat?

Posted on May 4, 2026

linuxgnomehackinggui

You, are wise.
You like to properly utilize your screen space.
You don’t like the massive margins in your desktop as a whole.
But you also have a high DPI screen.
The text you see is tiny.
When you increase the display scale, the text becomes readable,
but the huge margins repulse you.
But you, are wise.
You use the display scale minimally,
and increase the text size in accessibility settings instead.
The text is now big and the margins small.
All is good.
Except…
There is this icon in your top panel
indicating that an accessibility setting is active.
Well of course there is: it’s the enlarged text.
And it always will be.
You, are wise.
You do not like wasted screen space.
You do not like always-on superfluous non-informational icons in your sight.
And so begins your journey
to mask this ever-present actor that is the icon,
from the stage that is your desktop.

If You Were Naïve

An actor? That’s easy. Just hide() it during the Masking
and show() it during the Unmasking.
Two simple steps, to expand your pixel room.

const actorToMask = /* the accessibility icon. */;

function mask() {
    actorToMask.hide();
}

function unmask() {
    actorToMask.show();
}

That is what a fool would reason.
But you, are not a fool.
You, are wise.
You wonder what would happen
should Masking occur while the icon is already hidden.
Nothing, really.
But Unmasking, that is where it falls.
Unmasking show()s the icon.
Unmasking makes visible what never was.
A flaw!

Remembering Initial State

Close shave.
A fool would have carried on with a flawed Masking.
An intellectual would realize the flaw and remember the initial state.
In perhaps a binary switch they call a boolean.
Save the state and hide() during Masking.
Restore the state and show() during Unmasking.
Four simple steps, to expand you pixel room.

const actorToMask = /* the accessibility icon. */;
let showOnRestore = null;

function mask() {
    showOnRestore = actorToMask.is_visible();
    actorToMask.hide();
}

function unmask() {
    if (showOnRestore) {
        actorToMask.show();
    }
    showOnRestore = null;
}

That is what an intellectual would reason.
But you, aren’t just an intellectual.
You, are wise.
You begin to brood on what would happen
should some other thing hide() or show() the actor
between the Masking and Unmasking.
The mask hides what should happen.
And an initial state isn’t enough.
It remembers too little.

Handling External Changes

Signals, you remember.
Those watchful events that miss nothing
and fire when objects change.
Docs, you consult.
Two signals — show and hide — you find.
Easy it is, then.
Just update showOnRestore in response to these signals,
and hide() the actor in response to show.
We want to remember the visibility, but not reflect it just yet.

...
let showHandlerId = null;
let hideHandlerId = null;

function mask() {
    ...
    showHandlerId = actorToMask.connect('show', () => {
        // Remember to show it later,
        showOnRestore = true;
        // but keep it hidden right now.
        actorToMask.hide();
    });
    hideHandlerId = actorToMask.connect('hide', () => {
        showOnRestore = false;
    });
}

function unmask() {
    ...
    if (showHandlerId != null) {
        actorToMask.disconnect(showHandlerId);
        showHandlerId = null;
    }
    if (hideHandlerId != null) {
        actorToMask.disconnect(hideHandlerId);
        hideHandlerId = null;
    }
}

Your mask prevails.
Nothing gets shown.
A few more steps, to expand your pixel room.

Self-Loops

You, are wise.
Your cranium, large.
And so you wonder “What if the thing
…is me?”
Who-so-ever shall show() the actor,
shall trigger your show handler,
shall hide() the actor in response,
shall trigger your hide handler.
Inescapable, you realise;
a hide() triggers a hide,
but so does a show(),
and whether it’s you or another who caused a hide,
you may never know.
This is where many would lose,
where their efforts conclude.
But not to worry,
for you, are wise.

Pondering upon it, you thus realise:
There isn’t a need to remember!
What if instead, in every show response, you remember a pending hide?
And what if in response to a hide, you check if there is one pending?
An expectation.
One that short-circuits the handler if there’s a hide incoming.

...
let pendingHides = 0;

function mask() {
    ...
    showHandlerId = actorToMask.connect('show', () => {
        pendingHides++;
        ...
    });
    hideHandlerId = actorToMask.connect('hide', () => {
        if (pendingHides > 0) {
          pendingHides--;
          return;
        }
        ...
    });
}

function unmask() {
    ...
    pendingHides = 0;
}

Now who-so-ever shall show() the actor,
shall trigger your show handler,
shall register an[other] expected hide,
shall hide() the actor in response,
shall trigger your hide handler,
shall notice a hide was expected,
shall consider the/an expectation “met”,
and return early.
And a counter, of course, not a boolean.
For the asynchrony of life does not guarantee an expected hide to arrive
before another hide()es the actor, triggering a new hide.
Easy to miss for some.
But not you,
for you, are wise.

Livelocks

Thus more, you realise
that you may not be the only one force-hiding an actor.
That there could be another, force-showing that actor.
That this interplay of hide()s and hides and show()s and shows
may cause a livelock,
where the first hide()s it, the second reacts and show()s it,
the first reacts and hides()s it again, the second reacts and shows()s it again,
and forever it goes.
A frozen desktop, a jammed system;
not what you signed up for.
But you feel a light in your eyes,
for you, are wise.
You add a cutoff count;
a limit to how many times you reactively hide() the actor.
Had you been fancy, you would’ve added a timer.
The count would’ve become a rate instead.
A measure of how quickly you’re reactively hide()ing the actor.
Too much, and you cut your efforts off for a while.
Or stop tracking the actor, and add it to the log file.
But you, aren’t that fancy.
You understand the situation and settle with simplicity.
Because you, are lazy wise.

...
const MAX_OVERRIDES = 10;
let overrideCount = 0;

function mask() {
    ...
    showHandlerId = actorToMask.connect('show', () => {
        overrideCount++;
        if (overrideCount > MAX_OVERRIDES) {
            console.warn("Max overrides reached. Too many show()s while masked.");
            // Untrack. Unmask. Anything...
            return;
        }
        ...
    });
    ...
}

function unmask() {
    ...
    overrideCount = 0;
}

Sweet!
That went smooth.
Skeptics may say suspiciously smooth.
But not you.
You build, deploy and restart,
and that’s when you realise…

Uh, Oh…

Maybe you aren’t that wise.
Before your eyes lies the worst of all blocks,
as it dawns on you that you may have misinterpreted the docs.

show: The signal is emitted when an actor is visible and rendered on the stage.
hide: The signal is emitted when an actor is no longer rendered on the stage.

The signals aren’t triggered on every hide() and show(),
only on the first ones when called in a row.
Fatal oversight.
Maybe the skeptics were right.
But you’re in it too deep,
and can’t leave without a fight.

During the Masking,
you hide() the actor,
becoming blind to any future hide()s.
When a thing shows() the actor,
you react and hide() it,
immediately getting a hide,
once again becoming blind.
And on it goes,
until the Unmasking.
Ruminating, you face the truth:
Pending hides aren’t effective anymore.
Handling hides is pointless so you do it no more.
But what about showOnRestore?
For it’s the journey’s core.
And that’s when you see the end.
For you must return to your steps,
those initial four.

Conclusion

With much less information about when the actor is show()n or hide()n,
you are severely limited in what you can “remember” during the mask.
But there is one case,
and only one case,
where you can be sure of something.
If the actor was hidden at the time of Masking
and you never received a show,
it must stay hidden after Unmasking,
and that is all you can know.
Any other case and you can’t be sure,
so you play it safe and call a show().

const actorToMask = /* the accessibility icon. */;
let showOnRestore = null;
let showHandlerId = null;

const MAX_OVERRIDES = 10;
let overrideCount = 0;

function mask() {
    showOnRestore = actorToMask.is_visible();
    actorToMask.hide();

    showHandlerId = actorToMask.connect('show', () => {
        overrideCount++;
        if (overrideCount > MAX_OVERRIDES) {
            console.warn("Max overrides reached. Too many show()s while masked.");
            // Untrack. Unmask. Anything...
            return;
        }

        showOnRestore = true;
        actorToMask.hide();
    });
}

function unmask() {
    if (showOnRestore) {
        actorToMask.show();
    }
    showOnRestore = null;

    if (showHandlerId != null) {
        actorToMask.disconnect(showHandlerId);
        showHandlerId = null;
    }

    overrideCount = 0;
}

What remains is a limit for overrides,
and reactive calls to hide()s,
that one sure case is nice,
so you settle with a compromise.
Before you now, sits your prize.
sowing a little glimmer in your eyes.
Overcoming a hurdle great in size.
Got there in the end and it took a lot of tries.
But not to worry, for you
are only human.


Epilogue

A tastefully humble end :). If you learned something new, I’m glad to be of help. For many however, this might’ve just been stuff they already knew written in a slightly quirky way. As such, this wasn’t really a great candidate for an article on its own. But it had been marinating in my “outlined articles” folder for long enough that it felt wrong to just delete it. Hence, this. A slightly informative piece of text written in a slightly offbeat fashion. I enjoyed writing it. Hope you enjoyed reading it. If you’re from the GObject world, especially a GNOME Shell extension author, you might’ve recognized some bits of the code. And you’d be right: all code in this text is valid Shell extension code. I went through this journey while writing an extension to remove unnecessary visual elements from my desktop, combining functionality from Just Perfection and Quick Settings Tweaks, two extensions I barely used, replacing them with a simpler, custom-made one, despite their eminence; though you may use these concepts anytime, when masking GUI elements.