Skip to content

Conversation

@tarek-y-ismail
Copy link
Contributor

@tarek-y-ismail tarek-y-ismail commented Feb 4, 2026

Closes #3274

What's new?

  • Adds code BasicWindowManager to hide/restore attached surfaces in the "above" layer when a surface is fullscreened, unfullscreened, and removed.

How to test

Scenario 1: waybar on the above layer + fullscreen app

  1. Run mir with layer shell:
miral-app --add-wayland-extensions=zwlr_layer_shell_v1
  1. Set waybar to be on the "bottom" layer (~/.config/waybar/config):
{
    "layer": "bottom", // Waybar at top layer
   ...
}
  1. Start an application, a terminal for example
  2. Maximize it, it should take up all screen area until it hits waybar.
  3. Fullscreen it, it should take up the whole output
  4. Change the layer configuration waybar's config to be "top"
  5. Repeat step 4. Behavior should be identical
  6. Repeat step 5. Behavior should be identical

Scenario 2: Multiple outputs

  1. Start mir with multiple virtual outputs:
   miral-app --add-wayland-extensions=zwlr_layer_shell_v1  --x11-output=960x500:400x400
  1. Start a terminal via CTRL + ALT + t
  2. Make sure waybar is configured on the top layer and start it
  3. Fullscreen the terminal via F11
  4. Waybar should be hidden on the output where the terminal is fullscreened, and still visible on the other output
  5. Un-fullscreen and move the terminal to the other output, then re-fullscreen
  6. Behavior should be similar to step 5.

Scenario 3: Fullscreen app and OSK

  1. Run mir with all extensions:
miral-app --add-wayland-extensions=all
  1. Start a terminal via CTRL + ALT + t, run ubuntu-frame-osk
  2. Start kate, open any text file. OSK should pop up.
  3. Press F11 to fullscreen
  4. OSK should be hidden.

Preferably, we'd like the OSK to be visible when an app is fullscreened, but since it's placed in the "above" layer, and we have no way in the layer shell protocol to identify it, it will be broken for now.

Scenario 4: Waybar + Fullscreen app + non-fullscreen app
This follows scenario 1, with the following extra steps

  1. Run a different fullscreen app, a terminal via CTRL + ALT + t for example
  2. Focus should switch to it, way bar should be visible once more
  3. If you use the alt-tab switcher, waybar should hide and be restored as you focus on the fullscreened and non-fullscreened apps respectively.
  4. Changing focus via the pointer should behave similarly to step 3

Scenario 5: Waybar + Fullscreen app exits
Repeat scenario 1. Then close the app via a shortcut. If using a terminal, CTRL + D. Waybar should pop into view once more.

Scenario 5: Waybar + Fullscreen app with child surfaces

  1. Repeat scenario 1, but with VLC.
  2. With VLC fullscreened, open any entry from the menu bar
  3. Waybar should remain hidden
  4. Open a dialog via Media > Open File or Ctrl + o,
  5. Way bar should remain hidden.

Scenario 6:
Same as scenario 4, but instead of switching focus between the two apps, you exit the non-fullscreen app first.

Focus should switch to the fullscreen app, and waybar should remain hidden.

Checklist

  • Tests added and pass (coming (if possible) once this approach is approved)
  • Adequate documentation added (coming (if possible) once this approach is approved)

@tarek-y-ismail tarek-y-ismail force-pushed the MIRENG-457/hide-attached-above-surfaces-when-fullscreened branch from e76e32d to 4a67dd1 Compare February 4, 2026 11:37
@tarek-y-ismail tarek-y-ismail marked this pull request as ready for review February 4, 2026 11:37
@tarek-y-ismail tarek-y-ismail requested a review from a team as a code owner February 4, 2026 11:37
Copy link
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests?

Comment on lines 332 to 333
bool const was_fullscreen{info.state() == mir_window_state_fullscreen};
if (was_fullscreen)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a variable (especially with scope beyond the if statement).

But... does this code belong here anyway? I mean, if the focus shifts to another fullscreen window we need to undo this work.

Instead, when a window (or no window) gets focus we should be checking the state of the attached windows and acting accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it. This is the latest we can do this check. It was added specifically for the case where you have a fullscreen window, and you close it without exiting fullscreen first.

In that case, a few lines later, the info for this window will be erased before we reach the focus checking code, so we won't be able to move this code there.

Copy link
Contributor

@mattkae mattkae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An initial ask!

states

They are attached! The previous state is constant for all of them!
Mir saw that the buffer is not mapped, but the client submitted a
buffer with a size, so it sets its status to restored.

`place_and_size_for_state` then sees that the attached client is not
overlapping with the application zone. Which is correct because
application zones shrink for attached surfaces with exclusion zones. It
then tries to center the surface in the application zone, causing
clients like waybar, ironbar, and gbar to appear in the middle of the
screen when they try submitting a buffer while hidden under a fullscreen
app.
@tarek-y-ismail
Copy link
Contributor Author

tarek-y-ismail commented Feb 6, 2026

Something I've just realized is that I haven't taken into account cases where focus changes (or a surface is closed) with the closed surface and the newly focused surface on different outputs/display areas. So far I've assumed both are on the same display area.


Another thing I should mention is that focus handling feels quite messy at the moment. For example, when we have two apps open. Let's call them A and B. If we close A, remove_window gets called which removes the window and updates the MRU window stack among other things. active_window() now returns B .

Context:

policy->advise_delete_window(info);
info_for(application).remove_window(info.window());
mru_active_windows.erase(info.window());
fullscreen_surfaces.erase(info.window());
for (auto& area : display_areas)
{
area->attached_windows.erase(info.window());
std::erase(area->hidden_attached_windows, info.window());
}
if (info.state() == mir_window_state_attached &&
info.exclusive_rect().is_set())
{
update_application_zones_and_attached_windows();
}
application->destroy_surface(info.window());
bool const prev_was_fullscreen{info.state() == mir_window_state_fullscreen};
// NB erase() invalidates info, but we want to keep access to "parent".
auto const parent = info.parent();
auto const prev_display_area = display_area_for(info);
erase(info);

auto miral::BasicWindowManager::active_window() const -> Window
{
return allow_active_window ? mru_active_windows.top() : Window{};
}

We also call refocus, for which itself traverses the MRU window stack from top to bottom, starting with B.

mru_active_windows.enumerate([&](miral::Window& window)
{
// select_active_window() calls set_focus_to() which updates mru_active_windows and changes window
auto const w = window;
for (auto const& workspace : workspaces_containing(w))
{
for (auto const& ww : workspaces_containing_window)
{
if (ww == workspace)
{
return !(new_focus = select_active_window(w));
}
}
}
return true;
});

Then that calls set_active_window, and gets the active window, which is B.

auto const prev_window = active_window();

Then we execute the rest of set_active_window, which at that point we don't have information about the window we just removed!


Edit: I just realized that I've worked on this alone for 120% of my time today. So there might be a solution to the mini-rant above that my neurons are just too fried to see right now.

When there would be a layer shell surface with and a fullscreen surface
on one output, and we click on empty space in the other output, this
would cause the hidden layer shell surface to erroneously be restored.
@tarek-y-ismail tarek-y-ismail self-assigned this Feb 10, 2026
@tarek-y-ismail
Copy link
Contributor Author

tarek-y-ismail commented Feb 10, 2026

Bug: With two outputs, if we:

  1. start a terminal (A),
  2. start waybar, full screen the terminal,
  3. and open a new terminal (B), waybar is restored as expected.
  4. When terminal B is dragged to the second output, I believe the first output should hide waybar again. Will see if I can fix this, but leaving this here in case I can't immediately do so.

Edit: After thinking about it properly, no. waybar should not be automatically hidden. It will be hidden again if we focus terminal A, but it doesn't need to be hidden when focus is not on it.

Copy link
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems messy.

I don't have a clear solution to offer, but trying to model a window state of "attached and hidden" as "hidden and in a list" introduces a lot of corner cases that might be avoided with another approach. (A separate "hidden" property or a new "attached and hidden" state.)

hidden_attached_windows.push_back(window);
bwm.set_state(info, mir_window_state_hidden);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mixing of logic across BasicWindowManager and DisplayArea seems inelegant.

Another way to divide the logic:

void miral::BasicWindowManager::DisplayArea::update_hidden_attached(std::function<bool(Window const& w) predicate)
{
    // Copy as `predicate` may modify `attached_windows` and invalidate the iterator.
    auto const attached_windows = this->attached_windows;
    decltype(hidden_attached_windows) hidden_attached_windows;

    for (auto& window : attached_windows)
    {
        if (predicate(window))
        {
            hidden_attached_windows.push_back(window);
        }
    }

    this->hidden_attached_windows = std:move(hidden_attached_windows);
}
            current_display_area->update_hidden_attached([this](Window const& w)
                {
                     auto& info = info_for(window);
                     if (info.depth_layer() != mir_depth_layer_above) return false;
                     set_state(info, mir_window_state_hidden);
                     return true;
                });

}
}

void miral::BasicWindowManager::DisplayArea::hide_all_attached(BasicWindowManager& bwm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is wrong - only some attached are hidden.

Comment on lines +58 to +65
auto current = window;
while (current)
{
auto& info = bwm.info_for(current);
if (info.state() == mir_window_state_fullscreen)
return true;
current = info.parent();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a weirdly written for loop?

Comment on lines +55 to +56
if (!window)
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant check?

rect.top_left.y = zone.top_left.y + 0.5 * std::max(DeltaY{}, zone.size.height - rect.size.height);
}

auto is_window_or_parent_fullscreen(miral::Window const& window, miral::BasicWindowManager& bwm) -> bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler as a BasicWindowManager member function?

Comment on lines +87 to +99
void miral::BasicWindowManager::DisplayArea::restore_all_attached(BasicWindowManager& bwm)
{
for (auto& window : hidden_attached_windows)
{
if(!window)
continue;

auto& info = bwm.info_for(window);
bwm.set_state(info, mir_window_state_attached);
}

hidden_attached_windows.clear();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name wrong:

  1. it is not all attached that are restored; and,
  2. "restored" is a window state distinct from "attached"

Comment on lines +277 to +281
// If a layer shell surface (waybar) tries to submit a buffer while
// its hidden (in favor of another fullscreen surface), don't try
// to modify its placement.
if (display_area_for(info)->is_hidden_attached(info))
mods.state().consume();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overbroad: it bans any transitions. E.g. a panel that decides to transition from "attached" to "restored" (and, maybe, change layer too).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows marked mir_window_state_fullscreen appear behind shell components

3 participants