From 584b144953ba0f715c02b54444a61674a4c99e8a Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 29 Jun 2021 19:51:26 -0600 Subject: [PATCH] WindowServer: Add basic virtual desktop support This creates a 2-dimensional array of WindowStack instances, one for each virtual desktop. The main desktop 0,0 is the main desktop, which is the desktop used for all stationary windows (e.g. taskbar, desktop). When adding windows to a desktop, stationary windows are always added to the main desktop. When composing the desktop, there are usually two WindowStacks involved. For stationary windows, the main desktop will be traversed, and for everything else the current virtual desktop will be iterated. Iteration is interweaved to preserve the correct order. During the transition animation, two WindowStacks will be iterated at the same time. --- Userland/Libraries/LibGfx/DisjointRectSet.h | 11 + Userland/Services/WindowServer/Compositor.cpp | 250 +++++++++-- Userland/Services/WindowServer/Compositor.h | 35 +- Userland/Services/WindowServer/Overlays.cpp | 36 ++ Userland/Services/WindowServer/Overlays.h | 22 + Userland/Services/WindowServer/Window.h | 4 + .../Services/WindowServer/WindowManager.cpp | 425 ++++++++++++++---- .../Services/WindowServer/WindowManager.h | 166 ++++++- .../Services/WindowServer/WindowStack.cpp | 33 +- Userland/Services/WindowServer/WindowStack.h | 106 ++--- .../Services/WindowServer/WindowSwitcher.cpp | 25 +- 11 files changed, 900 insertions(+), 213 deletions(-) diff --git a/Userland/Libraries/LibGfx/DisjointRectSet.h b/Userland/Libraries/LibGfx/DisjointRectSet.h index ccb17b30dfc..cafb9157f28 100644 --- a/Userland/Libraries/LibGfx/DisjointRectSet.h +++ b/Userland/Libraries/LibGfx/DisjointRectSet.h @@ -129,6 +129,17 @@ public: const Vector& rects() const { return m_rects; } Vector take_rects() { return move(m_rects); } + void translate_by(int dx, int dy) + { + for (auto& rect : m_rects) + rect.translate_by(dx, dy); + } + void translate_by(Gfx::IntPoint const& delta) + { + for (auto& rect : m_rects) + rect.translate_by(delta); + } + private: bool add_no_shatter(const IntRect&); void shatter(); diff --git a/Userland/Services/WindowServer/Compositor.cpp b/Userland/Services/WindowServer/Compositor.cpp index f851d64a88c..f6bbb4458fd 100644 --- a/Userland/Services/WindowServer/Compositor.cpp +++ b/Userland/Services/WindowServer/Compositor.cpp @@ -123,6 +123,9 @@ void Compositor::init_bitmaps() void Compositor::did_construct_window_manager(Badge) { auto& wm = WindowManager::the(); + + m_current_window_stack = &wm.current_window_stack(); + m_wallpaper_mode = mode_to_enum(wm.config()->read_entry("Background", "Mode", "center")); m_custom_background_color = Color::from_string(wm.config()->read_entry("Background", "Color", "")); @@ -131,6 +134,17 @@ void Compositor::did_construct_window_manager(Badge) compose(); } +Gfx::IntPoint Compositor::window_transition_offset(Window& window) +{ + if (WindowManager::is_stationary_window_type(window.type())) + return {}; + + if (window.is_moving_to_another_stack()) + return {}; + + return window.outer_stack()->transition_offset(); +} + void Compositor::compose() { auto& wm = WindowManager::the(); @@ -158,33 +172,39 @@ void Compositor::compose() auto dirty_screen_rects = move(m_dirty_screen_rects); + bool window_stack_transition_in_progress = m_transitioning_to_window_stack != nullptr; + // Mark window regions as dirty that need to be re-rendered - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& window) { + wm.for_each_visible_window_from_back_to_front([&](Window& window) { + auto transition_offset = window_transition_offset(window); auto frame_rect = window.frame().render_rect(); + auto frame_rect_on_screen = frame_rect.translated(transition_offset); for (auto& dirty_rect : dirty_screen_rects.rects()) { - auto invalidate_rect = dirty_rect.intersected(frame_rect); + auto invalidate_rect = dirty_rect.intersected(frame_rect_on_screen); if (!invalidate_rect.is_empty()) { auto inner_rect_offset = window.rect().location() - frame_rect.location(); - invalidate_rect.translate_by(-(frame_rect.location() + inner_rect_offset)); + invalidate_rect.translate_by(-(frame_rect.location() + inner_rect_offset + transition_offset)); window.invalidate_no_notify(invalidate_rect); m_invalidated_window = true; } } window.prepare_dirty_rects(); + if (window_stack_transition_in_progress) + window.dirty_rects().translate_by(transition_offset); return IterationDecision::Continue; }); // Any windows above or below a given window that need to be re-rendered // also require us to re-render that window's intersecting area, regardless // of whether that window has any dirty rectangles - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& window) { + wm.for_each_visible_window_from_back_to_front([&](Window& window) { auto& transparency_rects = window.transparency_rects(); if (transparency_rects.is_empty()) return IterationDecision::Continue; auto frame_rect = window.frame().render_rect(); auto& dirty_rects = window.dirty_rects(); - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& w) { + wm.for_each_visible_window_from_back_to_front([&](Window& w) { if (&w == &window) return IterationDecision::Continue; auto frame_rect2 = w.frame().render_rect(); @@ -311,8 +331,9 @@ void Compositor::compose() // This window doesn't intersect with any screens, so there's nothing to render return IterationDecision::Continue; } - auto frame_rect = window.frame().render_rect(); - auto window_rect = window.rect(); + auto transition_offset = window_transition_offset(window); + auto frame_rect = window.frame().render_rect().translated(transition_offset); + auto window_rect = window.rect().translated(transition_offset); auto frame_rects = frame_rect.shatter(window_rect); dbgln_if(COMPOSE_DEBUG, " window {} frame rect: {}", window.title(), frame_rect); @@ -323,8 +344,9 @@ void Compositor::compose() rect.for_each_intersected(frame_rects, [&](const Gfx::IntRect& intersected_rect) { Gfx::PainterStateSaver saver(painter); painter.add_clip_rect(intersected_rect); + painter.translate(transition_offset); dbgln_if(COMPOSE_DEBUG, " render frame: {}", intersected_rect); - window.frame().paint(screen, painter, intersected_rect); + window.frame().paint(screen, painter, intersected_rect.translated(-transition_offset)); return IterationDecision::Continue; }); } @@ -481,7 +503,7 @@ void Compositor::compose() compose_window(*fullscreen_window); fullscreen_window->clear_dirty_rects(); } else { - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& window) { + wm.for_each_visible_window_from_back_to_front([&](Window& window) { compose_window(window); window.clear_dirty_rects(); return IterationDecision::Continue; @@ -899,11 +921,14 @@ void Compositor::decrement_show_screen_number(Badge) } } -bool Compositor::any_opaque_window_above_this_one_contains_rect(const Window& a_window, const Gfx::IntRect& rect) +bool Compositor::any_opaque_window_above_this_one_contains_rect(Window& a_window, const Gfx::IntRect& rect) { + auto* window_stack = a_window.outer_stack(); + if (!window_stack) + return false; bool found_containing_window = false; bool checking = false; - WindowManager::the().window_stack().for_each_visible_window_from_back_to_front([&](Window& window) { + WindowManager::the().for_each_visible_window_from_back_to_front([&](Window& window) { if (&window == &a_window) { checking = true; return IterationDecision::Continue; @@ -942,6 +967,7 @@ void Compositor::overlay_rects_changed() invalidate_occlusions(); for (auto& rect : m_overlay_rects.rects()) invalidate_screen(rect); + start_compose_async_timer(); } void Compositor::recompute_overlay_rects() @@ -973,17 +999,20 @@ void Compositor::recompute_overlay_rects() void Compositor::recompute_occlusions() { auto& wm = WindowManager::the(); - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& window) { - if (wm.m_switcher.is_visible()) { - window.set_occluded(false); - } else { + bool is_switcher_visible = wm.m_switcher.is_visible(); + wm.for_each_window_stack([&](WindowStack& window_stack) { + window_stack.set_all_occluded(!is_switcher_visible); + return IterationDecision::Continue; + }); + if (!is_switcher_visible) { + wm.for_each_visible_window_from_back_to_front([&](Window& window) { if (any_opaque_window_above_this_one_contains_rect(window, window.frame().rect())) window.set_occluded(true); else window.set_occluded(false); - } - return IterationDecision::Continue; - }); + return IterationDecision::Continue; + }); + } if (m_overlay_rects_changed) { m_overlay_rects_changed = false; @@ -996,11 +1025,12 @@ void Compositor::recompute_occlusions() dbgln(" overlay: {}", rect); } + bool window_stack_transition_in_progress = m_transitioning_to_window_stack != nullptr; auto& main_screen = Screen::main(); if (auto* fullscreen_window = wm.active_fullscreen_window()) { // TODO: support fullscreen windows on all screens auto screen_rect = main_screen.rect(); - WindowManager::the().window_stack().for_each_visible_window_from_front_to_back([&](Window& w) { + wm.for_each_visible_window_from_front_to_back([&](Window& w) { auto& visible_opaque = w.opaque_rects(); auto& transparency_rects = w.transparency_rects(); auto& transparency_wallpaper_rects = w.transparency_wallpaper_rects(); @@ -1029,7 +1059,7 @@ void Compositor::recompute_occlusions() Gfx::DisjointRectSet visible_rects; visible_rects.add_many(Screen::rects()); bool have_transparent = false; - WindowManager::the().window_stack().for_each_visible_window_from_front_to_back([&](Window& w) { + wm.for_each_visible_window_from_front_to_back([&](Window& w) { w.transparency_wallpaper_rects().clear(); auto& visible_opaque = w.opaque_rects(); visible_opaque.clear(); @@ -1039,8 +1069,13 @@ void Compositor::recompute_occlusions() if (w.is_minimized()) return IterationDecision::Continue; + auto transition_offset = window_transition_offset(w); auto transparent_frame_render_rects = w.frame().transparent_render_rects(); auto opaque_frame_render_rects = w.frame().opaque_render_rects(); + if (window_stack_transition_in_progress) { + transparent_frame_render_rects.translate_by(transition_offset); + opaque_frame_render_rects.translate_by(transition_offset); + } Gfx::DisjointRectSet visible_opaque_rects; Screen::for_each([&](auto& screen) { auto screen_rect = screen.rect(); @@ -1061,10 +1096,12 @@ void Compositor::recompute_occlusions() visible_opaque = visible_rects.intersected(visible_opaque_rects); auto render_rect = w.frame().render_rect(); - + auto render_rect_on_screen = render_rect; + if (window_stack_transition_in_progress) + render_rect_on_screen.translate_by(transition_offset); Gfx::DisjointRectSet opaque_covering; bool found_this_window = false; - WindowManager::the().window_stack().for_each_visible_window_from_back_to_front([&](Window& w2) { + wm.for_each_visible_window_from_back_to_front([&](Window& w2) { if (!found_this_window) { if (&w == &w2) found_this_window = true; @@ -1074,17 +1111,28 @@ void Compositor::recompute_occlusions() if (w2.is_minimized()) return IterationDecision::Continue; - if (!render_rect.intersects(w2.frame().render_rect())) + auto w2_render_rect = w2.frame().render_rect(); + auto w2_render_rect_on_screen = w2_render_rect; + auto w2_transition_offset = window_transition_offset(w2); + if (window_stack_transition_in_progress) + w2_render_rect_on_screen.translate_by(w2_transition_offset); + if (!render_rect_on_screen.intersects(w2_render_rect_on_screen)) return IterationDecision::Continue; - auto opaque_rects = w2.frame().opaque_render_rects().intersected(render_rect); - auto transparent_rects = w2.frame().transparent_render_rects().intersected(render_rect); + auto opaque_rects = w2.frame().opaque_render_rects(); + auto transparent_rects = w2.frame().transparent_render_rects(); + if (window_stack_transition_in_progress) { + auto transition_offset_2 = window_transition_offset(w2); + opaque_rects.translate_by(transition_offset_2); + transparent_rects.translate_by(transition_offset_2); + } + opaque_rects = opaque_rects.intersected(render_rect_on_screen); + transparent_rects = transparent_rects.intersected(render_rect_on_screen); if (opaque_rects.is_empty() && transparent_rects.is_empty()) return IterationDecision::Continue; - for (auto& covering : opaque_rects.rects()) { opaque_covering.add(covering); - if (opaque_covering.contains(render_rect)) { + if (opaque_covering.contains(render_rect_on_screen)) { // This window (including frame) is entirely covered by another opaque window visible_opaque.clear(); transparency_rects.clear(); @@ -1156,7 +1204,7 @@ void Compositor::recompute_occlusions() if (have_transparent) { // Determine what transparent window areas need to render the wallpaper first - WindowManager::the().window_stack().for_each_visible_window_from_back_to_front([&](Window& w) { + wm.for_each_visible_window_from_back_to_front([&](Window& w) { auto& transparency_wallpaper_rects = w.transparency_wallpaper_rects(); if (w.is_minimized()) { transparency_wallpaper_rects.clear(); @@ -1184,7 +1232,7 @@ void Compositor::recompute_occlusions() dbgln(" wallpaper opaque: {}", r); } - wm.window_stack().for_each_visible_window_from_back_to_front([&](Window& w) { + wm.for_each_visible_window_from_back_to_front([&](Window& w) { auto window_frame_rect = w.frame().render_rect(); if (w.is_minimized() || window_frame_rect.is_empty() || w.screens().is_empty()) return IterationDecision::Continue; @@ -1237,4 +1285,148 @@ void Compositor::update_animations(Screen& screen, Gfx::DisjointRectSet& flush_r } } +void Compositor::create_window_stack_switch_overlay(WindowStack& target_stack) +{ + if (m_stack_switch_overlay_timer) { + // Cancel any timer, we're going to delete the overlay + m_stack_switch_overlay_timer->stop(); + m_stack_switch_overlay_timer = nullptr; + } + Screen::for_each([&](auto& screen) { + auto& screen_data = m_screen_data[screen.index()]; + screen_data.m_window_stack_switch_overlay = nullptr; // delete it first + screen_data.m_window_stack_switch_overlay = create_overlay(screen, target_stack); + screen_data.m_window_stack_switch_overlay->set_enabled(true); + return IterationDecision::Continue; + }); +} + +void Compositor::start_window_stack_switch_overlay_timer() +{ + if (m_stack_switch_overlay_timer) { + m_stack_switch_overlay_timer->stop(); + m_stack_switch_overlay_timer = nullptr; + } + m_stack_switch_overlay_timer = Core::Timer::create_single_shot( + 500, + [this] { + Screen::for_each([&](auto& screen) { + auto& screen_data = m_screen_data[screen.index()]; + screen_data.m_window_stack_switch_overlay = nullptr; + return IterationDecision::Continue; + }); + }, + this); + m_stack_switch_overlay_timer->start(); +} + +void Compositor::finish_window_stack_switch() +{ + VERIFY(m_transitioning_to_window_stack); + VERIFY(m_current_window_stack); + VERIFY(m_transitioning_to_window_stack != m_current_window_stack); + + m_current_window_stack->set_transition_offset({}, {}); + m_transitioning_to_window_stack->set_transition_offset({}, {}); + + auto* previous_window_stack = m_current_window_stack; + m_current_window_stack = m_transitioning_to_window_stack; + m_transitioning_to_window_stack = nullptr; + + m_window_stack_transition_animation = nullptr; + + auto& wm = WindowManager::the(); + if (!wm.m_switcher.is_visible()) + previous_window_stack->set_all_occluded(true); + wm.did_switch_window_stack({}, *previous_window_stack, *m_current_window_stack); + + invalidate_occlusions(); + + // Rather than invalidating the entire we could invalidate all render rectangles + // that are affected by the transition offset before and after changing it. + invalidate_screen(); + + start_window_stack_switch_overlay_timer(); +} + +void Compositor::switch_to_window_stack(WindowStack& new_window_stack) +{ + if (m_transitioning_to_window_stack) { + if (m_transitioning_to_window_stack == &new_window_stack) + return; + // A switch is in progress, but the user is impatient. Finish the transition instantly + finish_window_stack_switch(); + VERIFY(!m_window_stack_transition_animation); + // Now switch to the next target as usual + } + VERIFY(m_current_window_stack); + + if (&new_window_stack == m_current_window_stack) { + // So that the user knows which stack they're on, show the overlay briefly + create_window_stack_switch_overlay(*m_current_window_stack); + start_window_stack_switch_overlay_timer(); + return; + } + VERIFY(!m_transitioning_to_window_stack); + m_transitioning_to_window_stack = &new_window_stack; + + auto window_stack_size = Screen::bounding_rect().size(); + + int delta_x = 0; + if (new_window_stack.column() < m_current_window_stack->column()) + delta_x = window_stack_size.width(); + else if (new_window_stack.column() > m_current_window_stack->column()) + delta_x = -window_stack_size.width(); + int delta_y = 0; + if (new_window_stack.row() < m_current_window_stack->row()) + delta_y = window_stack_size.height(); + else if (new_window_stack.row() > m_current_window_stack->row()) { + delta_y = -window_stack_size.height(); + } + + m_transitioning_to_window_stack->set_transition_offset({}, { -delta_x, -delta_y }); + m_current_window_stack->set_transition_offset({}, {}); + + create_window_stack_switch_overlay(*m_transitioning_to_window_stack); + // We start the timer when the animation ends! + + VERIFY(!m_window_stack_transition_animation); + m_window_stack_transition_animation = Animation::create(); + m_window_stack_transition_animation->set_duration(250); + m_window_stack_transition_animation->on_update = [this, delta_x, delta_y](float progress, Gfx::Painter&, Screen&, Gfx::DisjointRectSet&) { + VERIFY(m_transitioning_to_window_stack); + VERIFY(m_current_window_stack); + + // Set transition offset for the window stack we're transitioning out of + auto previous_transition_offset_from = m_current_window_stack->transition_offset(); + Gfx::IntPoint transition_offset_from { (float)delta_x * progress, (float)delta_y * progress }; + if (previous_transition_offset_from == transition_offset_from) + return; + + { + // we need to render both, the existing dirty rectangles as well as where we're shifting to + auto translated_dirty_rects = m_dirty_screen_rects.clone(); + auto transition_delta = transition_offset_from - previous_transition_offset_from; + translated_dirty_rects.translate_by(transition_delta); + m_dirty_screen_rects.add(translated_dirty_rects.intersected(Screen::bounding_rect())); + } + m_current_window_stack->set_transition_offset({}, transition_offset_from); + + // Set transition offset for the window stack we're transitioning to + Gfx::IntPoint transition_offset_to { (float)-delta_x * (1.0f - progress), (float)-delta_y * (1.0f - progress) }; + m_transitioning_to_window_stack->set_transition_offset({}, transition_offset_to); + + invalidate_occlusions(); + + // Rather than invalidating the entire we could invalidate all render rectangles + // that are affected by the transition offset before and after changing it. + invalidate_screen(); + }; + + m_window_stack_transition_animation->on_stop = [this] { + finish_window_stack_switch(); + }; + m_window_stack_transition_animation->start(); +} + } diff --git a/Userland/Services/WindowServer/Compositor.h b/Userland/Services/WindowServer/Compositor.h index 3d9e719cb62..f869d9d51f5 100644 --- a/Userland/Services/WindowServer/Compositor.h +++ b/Userland/Services/WindowServer/Compositor.h @@ -22,6 +22,7 @@ class Cursor; class MultiScaleBitmaps; class Window; class WindowManager; +class WindowStack; enum class WallpaperMode { Tile, @@ -92,6 +93,27 @@ public: return IterationDecision::Continue; } + template + IterationDecision for_each_rendering_window_stack(F f) + { + VERIFY(m_current_window_stack); + IterationDecision decision = f(*m_current_window_stack); + if (decision != IterationDecision::Continue) + return decision; + if (m_transitioning_to_window_stack) + decision = f(*m_transitioning_to_window_stack); + return decision; + } + + [[nodiscard]] WindowStack& get_rendering_window_stacks(WindowStack*& transitioning_window_stack) + { + transitioning_window_stack = m_transitioning_to_window_stack; + return *m_current_window_stack; + } + + bool is_switching_window_stacks() const { return m_transitioning_to_window_stack != nullptr; } + void switch_to_window_stack(WindowStack&); + void did_construct_window_manager(Badge); const Gfx::Bitmap* cursor_bitmap_for_screenshot(Badge, Screen&) const; @@ -114,10 +136,14 @@ private: void start_compose_async_timer(); void recompute_overlay_rects(); void recompute_occlusions(); - bool any_opaque_window_above_this_one_contains_rect(const Window&, const Gfx::IntRect&); + bool any_opaque_window_above_this_one_contains_rect(Window&, const Gfx::IntRect&); void change_cursor(const Cursor*); void flush(Screen&); + Gfx::IntPoint window_transition_offset(Window&); void update_animations(Screen&, Gfx::DisjointRectSet& flush_rects); + void create_window_stack_switch_overlay(WindowStack&); + void start_window_stack_switch_overlay_timer(); + void finish_window_stack_switch(); RefPtr m_compose_timer; RefPtr m_immediate_compose_timer; @@ -139,6 +165,7 @@ private: OwnPtr m_cursor_back_painter; Gfx::IntRect m_last_cursor_rect; OwnPtr m_screen_number_overlay; + OwnPtr m_window_stack_switch_overlay; bool m_buffers_are_flipped { false }; bool m_screen_can_set_buffer { false }; bool m_cursor_back_is_valid { false }; @@ -197,6 +224,12 @@ private: RefPtr m_display_link_notify_timer; size_t m_display_link_count { 0 }; + WindowStack* m_current_window_stack { nullptr }; + WindowStack* m_transitioning_to_window_stack { nullptr }; + RefPtr m_window_stack_transition_animation; + OwnPtr m_stack_switch_overlay; + RefPtr m_stack_switch_overlay_timer; + size_t m_show_screen_number_count { 0 }; Optional m_custom_background_color; diff --git a/Userland/Services/WindowServer/Overlays.cpp b/Userland/Services/WindowServer/Overlays.cpp index c5cedf29076..f183375537d 100644 --- a/Userland/Services/WindowServer/Overlays.cpp +++ b/Userland/Services/WindowServer/Overlays.cpp @@ -311,4 +311,40 @@ RefPtr DndOverlay::create_bitmap(int scale_factor) return new_bitmap; } +void WindowStackSwitchOverlay::render_overlay_bitmap(Gfx::Painter& painter) +{ + // We should come up with a more elegant way to get the content rectangle + Gfx::IntRect content_rect({}, m_content_size); + content_rect.center_within({ {}, rect().size() }); + auto active_color = WindowManager::the().palette().active_window_border1(); + auto inactive_color = WindowManager::the().palette().inactive_window_border1(); + for (int y = 0; y < m_rows; y++) { + for (int x = 0; x < m_columns; x++) { + Gfx::IntRect rect { + content_rect.left() + x * (default_screen_rect_width + default_screen_rect_padding), + content_rect.top() + y * (default_screen_rect_height + default_screen_rect_padding), + default_screen_rect_width, + default_screen_rect_height + }; + bool is_target = y == m_target_row && x == m_target_column; + painter.fill_rect(rect, is_target ? active_color : inactive_color); + } + } +} + +WindowStackSwitchOverlay::WindowStackSwitchOverlay(Screen& screen, WindowStack& target_window_stack) + : m_rows((int)WindowManager::the().window_stack_rows()) + , m_columns((int)WindowManager::the().window_stack_columns()) + , m_target_row((int)target_window_stack.row()) + , m_target_column((int)target_window_stack.column()) +{ + m_content_size = { + m_columns * (default_screen_rect_width + default_screen_rect_padding) - default_screen_rect_padding, + m_rows * (default_screen_rect_height + default_screen_rect_padding) - default_screen_rect_padding, + }; + auto rect = calculate_frame_rect(Gfx::IntRect({}, m_content_size).inflated(2 * default_screen_rect_margin, 2 * default_screen_rect_margin)); + rect.center_within(screen.rect()); + set_rect(rect); +} + } diff --git a/Userland/Services/WindowServer/Overlays.h b/Userland/Services/WindowServer/Overlays.h index b675e9b0c95..f94a870bd0a 100644 --- a/Userland/Services/WindowServer/Overlays.h +++ b/Userland/Services/WindowServer/Overlays.h @@ -17,6 +17,7 @@ namespace WindowServer { class Screen; class Window; +class WindowStack; class Overlay { friend class Compositor; @@ -27,6 +28,7 @@ public: enum class ZOrder { WindowGeometry, Dnd, + WindowStackSwitch, ScreenNumber, }; [[nodiscard]] virtual ZOrder zorder() const = 0; @@ -168,4 +170,24 @@ private: Gfx::IntRect m_label_rect; }; +class WindowStackSwitchOverlay : public RectangularOverlay { +public: + static constexpr int default_screen_rect_width = 40; + static constexpr int default_screen_rect_height = 30; + static constexpr int default_screen_rect_margin = 16; + static constexpr int default_screen_rect_padding = 8; + + WindowStackSwitchOverlay(Screen&, WindowStack&); + + virtual ZOrder zorder() const override { return ZOrder::WindowStackSwitch; } + virtual void render_overlay_bitmap(Gfx::Painter&) override; + +private: + Gfx::IntSize m_content_size; + const int m_rows; + const int m_columns; + const int m_target_row; + const int m_target_column; +}; + } diff --git a/Userland/Services/WindowServer/Window.h b/Userland/Services/WindowServer/Window.h index 09fc32431d5..f6728da7f5b 100644 --- a/Userland/Services/WindowServer/Window.h +++ b/Userland/Services/WindowServer/Window.h @@ -330,6 +330,9 @@ public: frame().window_was_constructed({}); } + void set_moving_to_another_stack(bool value) { m_moving_to_another_stack = value; } + bool is_moving_to_another_stack() const { return m_moving_to_another_stack; } + private: Window(ClientConnection&, WindowType, int window_id, bool modal, bool minimizable, bool frameless, bool resizable, bool fullscreen, bool accessory, Window* parent_window = nullptr); Window(Core::Object&, WindowType); @@ -382,6 +385,7 @@ private: bool m_invalidated_frame { true }; bool m_hit_testing_enabled { true }; bool m_modified { false }; + bool m_moving_to_another_stack { false }; WindowTileType m_tiled { WindowTileType::None }; Gfx::IntRect m_untiled_rect; bool m_occluded { false }; diff --git a/Userland/Services/WindowServer/WindowManager.cpp b/Userland/Services/WindowServer/WindowManager.cpp index a2912affb67..039efac1bc7 100644 --- a/Userland/Services/WindowServer/WindowManager.cpp +++ b/Userland/Services/WindowServer/WindowManager.cpp @@ -39,6 +39,26 @@ WindowManager::WindowManager(Gfx::PaletteImpl const& palette) { s_the = this; + { + // Create the default window stacks + auto row_count = m_window_stacks.capacity(); + VERIFY(row_count > 0); + for (size_t row_index = 0; row_index < row_count; row_index++) { + auto row = adopt_own(*new RemoveReference()); + auto column_count = row->capacity(); + VERIFY(column_count > 0); + for (size_t column_index = 0; column_index < column_count; column_index++) + row->append(adopt_own(*new WindowStack(row_index, column_index))); + m_window_stacks.append(move(row)); + } + + m_current_window_stack = &m_window_stacks[0][0]; + for (auto& row : m_window_stacks) { + for (auto& stack : row) + stack.set_stationary_window_stack(*m_current_window_stack); + } + } + reload_config(); Compositor::the().did_construct_window_manager({}); @@ -123,9 +143,12 @@ bool WindowManager::set_screen_layout(ScreenLayout&& screen_layout, bool save, S client.notify_about_new_screen_rects(Screen::rects(), Screen::main().index()); }); - m_window_stack.for_each_window([](Window& window) { - window.screens().clear_with_capacity(); - window.recalculate_rect(); + for_each_window_stack([&](auto& window_stack) { + window_stack.for_each_window([](Window& window) { + window.screens().clear_with_capacity(); + window.recalculate_rect(); + return IterationDecision::Continue; + }); return IterationDecision::Continue; }); @@ -178,11 +201,23 @@ int WindowManager::double_click_speed() const return m_double_click_speed; } +WindowStack& WindowManager::window_stack_for_window(Window& window) +{ + if (is_stationary_window_type(window.type())) + return m_window_stacks[0][0]; + if (auto* parent = window.parent_window(); parent && !is_stationary_window_type(parent->type())) { + if (auto* parent_window_stack = parent->outer_stack()) + return *parent_window_stack; + } + return current_window_stack(); +} + void WindowManager::add_window(Window& window) { - bool is_first_window = m_window_stack.is_empty(); + auto& window_stack = window_stack_for_window(window); + bool is_first_window = window_stack.is_empty(); - m_window_stack.add(window); + window_stack.add(window); if (window.is_fullscreen()) { auto& screen = Screen::main(); // TODO: support fullscreen windows on other screens! @@ -235,7 +270,9 @@ void WindowManager::move_to_front_and_make_active(Window& window) void WindowManager::do_move_to_front(Window& window, bool make_active, bool make_input) { - m_window_stack.move_to_front(window); + auto* window_stack = window.outer_stack(); + VERIFY(window_stack); + window_stack->move_to_front(window); if (make_active) set_active_window(&window, make_input); @@ -256,11 +293,11 @@ void WindowManager::do_move_to_front(Window& window, bool make_active, bool make void WindowManager::remove_window(Window& window) { - check_hide_geometry_overlay(window); - m_window_stack.remove(window); auto* active = active_window(); auto* active_input = active_input_window(); + if (auto* window_stack = window.outer_stack()) + window_stack->remove(window); if (active == &window || active_input == &window || (active && window.is_descendant_of(*active)) || (active_input && active_input != active && window.is_descendant_of(*active_input))) pick_new_active_window(&window); @@ -285,11 +322,14 @@ void WindowManager::greet_window_manager(WMClientConnection& conn) if (conn.window_id() < 0) return; - m_window_stack.for_each_window([&](Window& other_window) { - //if (conn.window_id() != other_window.window_id()) { - tell_wm_about_window(conn, other_window); - tell_wm_about_window_icon(conn, other_window); - //} + for_each_window_stack([&](auto& window_stack) { + window_stack.for_each_window([&](Window& other_window) { + //if (conn.window_id() != other_window.window_id()) { + tell_wm_about_window(conn, other_window); + tell_wm_about_window_icon(conn, other_window); + //} + return IterationDecision::Continue; + }); return IterationDecision::Continue; }); if (auto* applet_area_window = AppletManager::the().window()) @@ -473,7 +513,8 @@ bool WindowManager::pick_new_active_window(Window* previous_active) { bool new_window_picked = false; Window* first_candidate = nullptr; - m_window_stack.for_each_visible_window_from_front_to_back([&](Window& candidate) { + + for_each_visible_window_from_front_to_back([&](Window& candidate) { if (candidate.type() != WindowType::Normal && candidate.type() != WindowType::ToolWindow) return IterationDecision::Continue; if (candidate.is_destroyed()) @@ -555,7 +596,7 @@ void WindowManager::start_window_resize(Window& window, Gfx::IntPoint const& pos m_geometry_overlay = Compositor::the().create_overlay(window); m_geometry_overlay->set_enabled(true); - m_active_input_tracking_window = nullptr; + current_window_stack().set_active_input_tracking_window(nullptr); window.invalidate(true, true); @@ -803,7 +844,7 @@ bool WindowManager::process_ongoing_drag(MouseEvent& event) m_dnd_overlay->cursor_moved(); // We didn't let go of the drag yet, see if we should send some drag move events.. - m_window_stack.for_each_visible_window_from_front_to_back([&](Window& window) { + for_each_visible_window_from_front_to_back([&](Window& window) { if (!window.rect().contains(event.position())) return IterationDecision::Continue; event.set_drag(true); @@ -816,7 +857,7 @@ bool WindowManager::process_ongoing_drag(MouseEvent& event) if (!(event.type() == Event::MouseUp && event.button() == MouseButton::Left)) return true; - if (auto* window = m_window_stack.window_at(event.position())) { + if (auto* window = current_window_stack().window_at(event.position())) { m_dnd_client->async_drag_accepted(); if (window->client()) { auto translated_event = event.translated(-window->position()); @@ -971,7 +1012,9 @@ void WindowManager::deliver_mouse_event(Window& window, MouseEvent const& event, bool WindowManager::process_ongoing_active_input_mouse_event(MouseEvent const& event) { - if (!m_active_input_tracking_window) + auto& window_stack = current_window_stack(); + auto* input_tracking_window = window_stack.active_input_tracking_window(); + if (!input_tracking_window) return false; // At this point, we have delivered the start of an input sequence to a @@ -980,11 +1023,10 @@ bool WindowManager::process_ongoing_active_input_mouse_event(MouseEvent const& e // // This prevents e.g. moving on one window out of the bounds starting // a move in that other unrelated window, and other silly shenanigans. - deliver_mouse_event(*m_active_input_tracking_window, event, true); + deliver_mouse_event(*input_tracking_window, event, true); - if (event.type() == Event::MouseUp && event.buttons() == 0) { - m_active_input_tracking_window = nullptr; - } + if (event.type() == Event::MouseUp && event.buttons() == 0) + window_stack.set_active_input_tracking_window(nullptr); return true; } @@ -1049,7 +1091,7 @@ void WindowManager::process_mouse_event_for_window(HitTestResult& result, MouseE } if (event.type() == Event::MouseDown) - m_active_input_tracking_window = window; + current_window_stack().set_active_input_tracking_window(&window); } void WindowManager::process_mouse_event(MouseEvent& event) @@ -1065,8 +1107,9 @@ void WindowManager::process_mouse_event(MouseEvent& event) // 2. Send the mouse event to all windows with global cursor tracking enabled. // The active input tracking window is excluded here because we're sending the event to it // in the next step. - m_window_stack.for_each_visible_window_from_front_to_back([&](Window& window) { - if (window.global_cursor_tracking() && &window != m_active_input_tracking_window) + auto& window_stack = current_window_stack(); + for_each_visible_window_from_front_to_back([&](Window& window) { + if (window.global_cursor_tracking() && &window != window_stack.active_input_tracking_window()) deliver_mouse_event(window, event, false); return IterationDecision::Continue; }); @@ -1112,7 +1155,7 @@ void WindowManager::process_mouse_event(MouseEvent& event) } // 8. Hit test the window stack to see what's under the cursor. - auto result = m_window_stack.hit_test(event.position()); + auto result = current_window_stack().hit_test(event.position()); if (!result.has_value()) { // No window is under the cursor. @@ -1144,7 +1187,7 @@ void WindowManager::reevaluate_hovered_window(Window* updated_window) if (fullscreen_window->hit_test(cursor_location).has_value()) hovered_window = fullscreen_window; } else { - hovered_window = m_window_stack.window_at(cursor_location); + hovered_window = current_window_stack().window_at(cursor_location); } if (set_hovered_window(hovered_window)) { @@ -1213,7 +1256,8 @@ void WindowManager::event(Core::Event& event) m_previous_event_was_super_keydown = false; process_mouse_event(mouse_event); - set_hovered_window(m_window_stack.window_at(mouse_event.position(), WindowStack::IncludeWindowFrame::No)); + // TODO: handle transitioning between two stacks + set_hovered_window(current_window_stack().window_at(mouse_event.position(), WindowStack::IncludeWindowFrame::No)); return; } @@ -1225,6 +1269,117 @@ void WindowManager::event(Core::Event& event) Core::Object::event(event); } +bool WindowManager::is_window_in_modal_stack(Window& window_in_modal_stack, Window& other_window) +{ + auto result = for_each_window_in_modal_stack(window_in_modal_stack, [&](auto& window, auto) { + if (&other_window == &window) + return IterationDecision::Break; + for (auto& accessory : window.accessory_windows()) { + if (accessory.ptr() == &other_window) + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + return result == IterationDecision::Break; +} + +void WindowManager::switch_to_window_stack(WindowStack& window_stack, Window* carry_window) +{ + m_carry_window_to_new_stack.clear(); + m_switching_to_window_stack = &window_stack; + if (carry_window && !is_stationary_window_type(carry_window->type()) && carry_window->outer_stack() != &window_stack) { + auto& from_stack = *carry_window->outer_stack(); + + auto* blocking_modal = carry_window->blocking_modal_window(); + for_each_visible_window_from_back_to_front([&](Window& window) { + if (is_stationary_window_type(window.type())) + return IterationDecision::Continue; + + if (window.outer_stack() != carry_window->outer_stack()) + return IterationDecision::Continue; + if (&window == carry_window || ((carry_window->is_modal() || blocking_modal) && is_window_in_modal_stack(*carry_window, window))) + m_carry_window_to_new_stack.append(window); + return IterationDecision::Continue; + }, + &from_stack); + + auto* from_active_window = from_stack.active_window(); + auto* from_active_input_window = from_stack.active_input_window(); + bool did_carry_active_window = false; + bool did_carry_active_input_window = false; + for (auto& window : m_carry_window_to_new_stack) { + if (window == from_active_window) + did_carry_active_window = true; + if (window == from_active_input_window) + did_carry_active_input_window = true; + window->set_moving_to_another_stack(true); + VERIFY(window->outer_stack() == &from_stack); + from_stack.remove(*window); + window_stack.add(*window); + } + // Before we change to the new stack, find a new active window on the stack we're switching from + if (did_carry_active_window || did_carry_active_input_window) + pick_new_active_window(from_active_window); + + // Now switch to the new stack + m_current_window_stack = &window_stack; + if (did_carry_active_window && from_active_window) + set_active_window(from_active_window, from_active_input_window == from_active_window); + if (did_carry_active_input_window && from_active_input_window && from_active_input_window != from_active_window) + set_active_input_window(from_active_input_window); + + // Because we moved windows between stacks we need to invalidate occlusions + Compositor::the().invalidate_occlusions(); + } else { + m_current_window_stack = &window_stack; + } + + Compositor::the().switch_to_window_stack(window_stack); +} + +void WindowManager::did_switch_window_stack(Badge, WindowStack& previous_stack, WindowStack& new_stack) +{ + VERIFY(&previous_stack != &new_stack); + + // We are being notified by the compositor, it should not be switching right now! + VERIFY(!Compositor::the().is_switching_window_stacks()); + VERIFY(¤t_window_stack() == &new_stack); + + if (m_switching_to_window_stack == &new_stack) { + m_switching_to_window_stack = nullptr; + if (!m_carry_window_to_new_stack.is_empty()) { + // switched_to_stack may be different from the stack where the windows were + // carried to when the user rapidly tries to switch stacks, so make sure to + // only reset the moving flag if we arrived at our final destination + for (auto& window_ref : m_carry_window_to_new_stack) { + if (auto* window = window_ref.ptr()) + window->set_moving_to_another_stack(false); + } + m_carry_window_to_new_stack.clear(); + } + } + + auto* previous_stack_active_window = previous_stack.active_window(); + auto* previous_stack_active_input_window = previous_stack.active_input_window(); + auto* new_stack_active_window = new_stack.active_window(); + auto* new_stack_active_input_window = new_stack.active_input_window(); + if (previous_stack_active_input_window && previous_stack_active_input_window != new_stack_active_input_window) + notify_previous_active_input_window(*previous_stack_active_input_window); + if (new_stack_active_input_window && previous_stack_active_input_window != new_stack_active_input_window) + notify_new_active_input_window(*new_stack_active_input_window); + if (previous_stack_active_window != new_stack_active_window) { + if (previous_stack_active_window && is_stationary_window_type(previous_stack_active_window->type())) + notify_previous_active_window(*previous_stack_active_window); + if (new_stack_active_window && is_stationary_window_type(new_stack_active_window->type())) + notify_new_active_window(*new_stack_active_window); + } + + if (!new_stack_active_input_window) + pick_new_active_window(nullptr); + + reevaluate_hovered_window(); +} + void WindowManager::process_key_event(KeyEvent& event) { m_keyboard_modifiers = event.modifiers(); @@ -1258,7 +1413,7 @@ void WindowManager::process_key_event(KeyEvent& event) m_previous_event_was_super_keydown = true; } else if (m_previous_event_was_super_keydown) { m_previous_event_was_super_keydown = false; - if (!m_dnd_client && !m_active_input_tracking_window && event.type() == Event::KeyUp && event.key() == Key_Super) { + if (!m_dnd_client && !current_window_stack().active_input_tracking_window() && event.type() == Event::KeyUp && event.key() == Key_Super) { tell_wms_super_key_pressed(); return; } @@ -1281,59 +1436,110 @@ void WindowManager::process_key_event(KeyEvent& event) return; } - if (!m_active_input_window) - return; - - if (event.type() == Event::KeyDown && event.modifiers() == Mod_Super && m_active_input_window->type() != WindowType::Desktop) { - if (event.key() == Key_Down) { - if (m_active_input_window->is_resizable() && m_active_input_window->is_maximized()) { - maximize_windows(*m_active_input_window, false); - return; + if (event.type() == Event::KeyDown && (event.modifiers() == (Mod_Ctrl | Mod_Alt) || event.modifiers() == (Mod_Ctrl | Mod_Shift | Mod_Alt)) && (window_stack_columns() > 1 || window_stack_rows() > 1)) { + auto& current_stack = current_window_stack(); + auto row = current_stack.row(); + auto column = current_stack.column(); + auto handle_window_stack_switch_key = [&]() { + switch (event.key()) { + case Key_Left: + if (column == 0) + return true; + column--; + return true; + case Key_Right: + if (column + 1 >= m_window_stacks[0].size()) + return true; + column++; + return true; + case Key_Up: + if (row == 0) + return true; + row--; + return true; + case Key_Down: + if (row + 1 >= m_window_stacks.size()) + return true; + row++; + return true; + default: + return false; } - if (m_active_input_window->is_minimizable()) - minimize_windows(*m_active_input_window, true); + }; + if (handle_window_stack_switch_key()) { + Window* carry_window = nullptr; + auto& new_window_stack = m_window_stacks[row][column]; + if (&new_window_stack != ¤t_stack) { + if (event.modifiers() == (Mod_Ctrl | Mod_Shift | Mod_Alt)) + carry_window = this->active_window(); + } + // Call switch_to_window_stack even if we're not going to switch to another stack. + // We'll show the window stack switch overlay briefly! + switch_to_window_stack(new_window_stack, carry_window); return; } - if (m_active_input_window->is_resizable()) { + } + + auto* active_input_window = current_window_stack().active_input_window(); + if (!active_input_window) + return; + + if (event.type() == Event::KeyDown && event.modifiers() == Mod_Super && active_input_window->type() != WindowType::Desktop) { + if (event.key() == Key_Down) { + if (active_input_window->is_resizable() && active_input_window->is_maximized()) { + maximize_windows(*active_input_window, false); + return; + } + if (active_input_window->is_minimizable()) + minimize_windows(*active_input_window, true); + return; + } + if (active_input_window->is_resizable()) { if (event.key() == Key_Up) { - maximize_windows(*m_active_input_window, !m_active_input_window->is_maximized()); + maximize_windows(*active_input_window, !active_input_window->is_maximized()); return; } if (event.key() == Key_Left) { - if (m_active_input_window->tiled() == WindowTileType::Left) + if (active_input_window->tiled() == WindowTileType::Left) return; - if (m_active_input_window->tiled() != WindowTileType::None) { - m_active_input_window->set_untiled(); + if (active_input_window->tiled() != WindowTileType::None) { + active_input_window->set_untiled(); return; } - if (m_active_input_window->is_maximized()) - maximize_windows(*m_active_input_window, false); - m_active_input_window->set_tiled(nullptr, WindowTileType::Left); + if (active_input_window->is_maximized()) + maximize_windows(*active_input_window, false); + active_input_window->set_tiled(nullptr, WindowTileType::Left); return; } if (event.key() == Key_Right) { - if (m_active_input_window->tiled() == WindowTileType::Right) + if (active_input_window->tiled() == WindowTileType::Right) return; - if (m_active_input_window->tiled() != WindowTileType::None) { - m_active_input_window->set_untiled(); + if (active_input_window->tiled() != WindowTileType::None) { + active_input_window->set_untiled(); return; } - if (m_active_input_window->is_maximized()) - maximize_windows(*m_active_input_window, false); - m_active_input_window->set_tiled(nullptr, WindowTileType::Right); + if (active_input_window->is_maximized()) + maximize_windows(*active_input_window, false); + active_input_window->set_tiled(nullptr, WindowTileType::Right); return; } } } - m_active_input_window->dispatch_event(event); + active_input_window->dispatch_event(event); } void WindowManager::set_highlight_window(Window* new_highlight_window) { - if (new_highlight_window == m_window_stack.highlight_window()) + // NOTE: The highlight window is global across all stacks. That's because we + // can only have one and we want to be able to highlight it during transitions + auto* previous_highlight_window = highlight_window(); + if (new_highlight_window == previous_highlight_window) return; - auto* previous_highlight_window = m_window_stack.highlight_window(); - m_window_stack.set_highlight_window(new_highlight_window); + if (!new_highlight_window) + m_highlight_window = nullptr; + else + m_highlight_window = new_highlight_window->make_weak_ptr(); + if (previous_highlight_window) { previous_highlight_window->invalidate(true, true); Compositor::the().invalidate_screen(previous_highlight_window->frame().render_rect()); @@ -1377,23 +1583,31 @@ void WindowManager::restore_active_input_window(Window* window) Window* WindowManager::set_active_input_window(Window* window) { - if (window == m_active_input_window) + auto& window_stack = current_window_stack(); + auto* previous_input_window = window_stack.active_input_window(); + if (window == previous_input_window) return window; - Window* previous_input_window = m_active_input_window; if (previous_input_window) - Core::EventLoop::current().post_event(*previous_input_window, make(Event::WindowInputLeft)); + notify_previous_active_input_window(*previous_input_window); - if (window) { - m_active_input_window = *window; - Core::EventLoop::current().post_event(*window, make(Event::WindowInputEntered)); - } else { - m_active_input_window = nullptr; - } + window_stack.set_active_input_window(window); + if (window) + notify_new_active_input_window(*window); return previous_input_window; } +void WindowManager::notify_new_active_input_window(Window& new_input_window) +{ + Core::EventLoop::current().post_event(new_input_window, make(Event::WindowInputEntered)); +} + +void WindowManager::notify_previous_active_input_window(Window& previous_input_window) +{ + Core::EventLoop::current().post_event(previous_input_window, make(Event::WindowInputLeft)); +} + void WindowManager::set_active_window(Window* new_active_window, bool make_input) { if (new_active_window) { @@ -1418,32 +1632,44 @@ void WindowManager::set_active_window(Window* new_active_window, bool make_input if (make_input) set_active_input_window(new_active_input_window); - if (new_active_window == m_window_stack.active_window()) + auto& window_stack = current_window_stack(); + if (new_active_window == window_stack.active_window()) return; - if (auto* previously_active_window = m_window_stack.active_window()) { - for (auto& child_window : previously_active_window->child_windows()) { - if (child_window && child_window->type() == WindowType::Tooltip) - child_window->request_close(); - } - Core::EventLoop::current().post_event(*previously_active_window, make(Event::WindowDeactivated)); - previously_active_window->invalidate(true, true); - m_window_stack.set_active_window(nullptr); - m_active_input_tracking_window = nullptr; - tell_wms_window_state_changed(*previously_active_window); + if (auto* previously_active_window = window_stack.active_window()) { + window_stack.set_active_window(nullptr); + window_stack.set_active_input_tracking_window(nullptr); + notify_previous_active_window(*previously_active_window); } if (new_active_window) { - m_window_stack.set_active_window(new_active_window); - Core::EventLoop::current().post_event(*new_active_window, make(Event::WindowActivated)); - new_active_window->invalidate(true, true); - tell_wms_window_state_changed(*new_active_window); + window_stack.set_active_window(new_active_window); + notify_new_active_window(*new_active_window); } // Window shapes may have changed (e.g. shadows for inactive/active windows) Compositor::the().invalidate_occlusions(); } +void WindowManager::notify_new_active_window(Window& new_active_window) +{ + Core::EventLoop::current().post_event(new_active_window, make(Event::WindowActivated)); + new_active_window.invalidate(true, true); + tell_wms_window_state_changed(new_active_window); +} + +void WindowManager::notify_previous_active_window(Window& previously_active_window) +{ + for (auto& child_window : previously_active_window.child_windows()) { + if (child_window && child_window->type() == WindowType::Tooltip) + child_window->request_close(); + } + Core::EventLoop::current().post_event(previously_active_window, make(Event::WindowDeactivated)); + previously_active_window.invalidate(true, true); + + tell_wms_window_state_changed(previously_active_window); +} + bool WindowManager::set_hovered_window(Window* window) { if (m_hovered_window == window) @@ -1461,7 +1687,7 @@ bool WindowManager::set_hovered_window(Window* window) ClientConnection const* WindowManager::active_client() const { - if (auto* window = m_window_stack.active_window()) + if (auto* window = const_cast(this)->current_window_stack().active_window()) return window->client(); return nullptr; } @@ -1534,7 +1760,7 @@ Gfx::IntRect WindowManager::maximized_window_rect(Window const& window, bool rel if (screen.is_main_screen()) { // Subtract taskbar window height if present - const_cast(this)->m_window_stack.for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, [&rect](Window& taskbar_window) { + const_cast(this)->current_window_stack().for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, [&rect](Window& taskbar_window) { rect.set_height(rect.height() - taskbar_window.height()); return IterationDecision::Break; }); @@ -1558,7 +1784,7 @@ void WindowManager::start_dnd_drag(ClientConnection& client, String const& text, m_dnd_overlay->set_enabled(true); m_dnd_mime_data = mime_data; Compositor::the().invalidate_cursor(); - m_active_input_tracking_window = nullptr; + current_window_stack().set_active_input_tracking_window(nullptr); } void WindowManager::end_dnd_drag() @@ -1574,8 +1800,11 @@ void WindowManager::invalidate_after_theme_or_font_change() { Compositor::the().set_background_color(m_config->read_entry("Background", "Color", palette().desktop_background().to_string())); WindowFrame::reload_config(); - m_window_stack.for_each_window([&](Window& window) { - window.frame().theme_changed(); + for_each_window_stack([&](auto& window_stack) { + window_stack.for_each_window([&](Window& window) { + window.frame().theme_changed(); + return IterationDecision::Continue; + }); return IterationDecision::Continue; }); ClientConnection::for_each_client([&](ClientConnection& client) { @@ -1604,10 +1833,11 @@ bool WindowManager::update_theme(String theme_path, String theme_name) void WindowManager::did_popup_a_menu(Badge) { // Clear any ongoing input gesture - if (!m_active_input_tracking_window) + auto* active_input_tracking_window = current_window_stack().active_input_tracking_window(); + if (!active_input_tracking_window) return; - m_active_input_tracking_window->set_automatic_cursor_tracking_enabled(false); - m_active_input_tracking_window = nullptr; + active_input_tracking_window->set_automatic_cursor_tracking_enabled(false); + active_input_tracking_window = nullptr; } void WindowManager::minimize_windows(Window& window, bool minimized) @@ -1638,7 +1868,7 @@ Gfx::IntPoint WindowManager::get_recommended_window_position(Gfx::IntPoint const int taskbar_height = 28; Window const* overlap_window = nullptr; - m_window_stack.for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, [&](Window& window) { + current_window_stack().for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, [&](Window& window) { if (window.default_positioned() && (!overlap_window || overlap_window->window_id() < window.window_id())) { overlap_window = &window; } @@ -1663,9 +1893,12 @@ Gfx::IntPoint WindowManager::get_recommended_window_position(Gfx::IntPoint const void WindowManager::reload_icon_bitmaps_after_scale_change() { reload_config(); - m_window_stack.for_each_window([&](Window& window) { - auto& window_frame = window.frame(); - window_frame.theme_changed(); + for_each_window_stack([&](auto& window_stack) { + window_stack.for_each_window([&](Window& window) { + auto& window_frame = window.frame(); + window_frame.theme_changed(); + return IterationDecision::Continue; + }); return IterationDecision::Continue; }); } @@ -1679,4 +1912,10 @@ void WindowManager::set_window_with_active_menu(Window* window) else m_window_with_active_menu = nullptr; } + +WindowStack& WindowManager::get_rendering_window_stacks(WindowStack*& transitioning_window_stack) +{ + return Compositor::the().get_rendering_window_stacks(transitioning_window_stack); +} + } diff --git a/Userland/Services/WindowServer/WindowManager.h b/Userland/Services/WindowServer/WindowManager.h index 3dfa0b6ab6e..6d1b64e5724 100644 --- a/Userland/Services/WindowServer/WindowManager.h +++ b/Userland/Services/WindowServer/WindowManager.h @@ -90,17 +90,36 @@ public: void start_dnd_drag(ClientConnection&, String const& text, Gfx::Bitmap const*, Core::MimeData const&); void end_dnd_drag(); - Window* active_window() { return m_window_stack.active_window(); } - Window const* active_window() const { return m_window_stack.active_window(); } - Window* active_input_window() { return m_active_input_window.ptr(); } - Window const* active_input_window() const { return m_active_input_window.ptr(); } + Window* active_window() + { + VERIFY(m_current_window_stack); + return m_current_window_stack->active_window(); + } + Window const* active_window() const + { + VERIFY(m_current_window_stack); + return m_current_window_stack->active_window(); + } + + Window* active_input_window() + { + VERIFY(m_current_window_stack); + return m_current_window_stack->active_input_window(); + } + Window const* active_input_window() const + { + VERIFY(m_current_window_stack); + return m_current_window_stack->active_input_window(); + } + ClientConnection const* active_client() const; Window* window_with_active_menu() { return m_window_with_active_menu; } Window const* window_with_active_menu() const { return m_window_with_active_menu; } void set_window_with_active_menu(Window*); - Window const* highlight_window() const { return m_window_stack.highlight_window(); } + Window* highlight_window() { return m_highlight_window; } + Window const* highlight_window() const { return m_highlight_window; } void set_highlight_window(Window*); void move_to_front_and_make_active(Window&); @@ -223,6 +242,7 @@ public: return f(window, true); } } + bool is_window_in_modal_stack(Window& window_in_modal_stack, Window& other_window); Gfx::IntPoint get_recommended_window_position(Gfx::IntPoint const& desired); @@ -231,13 +251,61 @@ public: void reevaluate_hovered_window(Window* = nullptr); Window* hovered_window() const { return m_hovered_window.ptr(); } - WindowStack& window_stack() { return m_window_stack; } + void switch_to_window_stack(WindowStack&, Window* = nullptr); + + size_t window_stack_rows() const { return m_window_stacks.size(); } + size_t window_stack_columns() const { return m_window_stacks[0].size(); } + + WindowStack& current_window_stack() + { + VERIFY(m_current_window_stack); + return *m_current_window_stack; + } + + template + IterationDecision for_each_window_stack(F f) + { + for (auto& row : m_window_stacks) { + for (auto& stack : row) { + IterationDecision decision = f(stack); + if (decision != IterationDecision::Continue) + return decision; + } + } + return IterationDecision::Continue; + } + + WindowStack& window_stack_for_window(Window&); + + static constexpr bool is_stationary_window_type(WindowType window_type) + { + switch (window_type) { + case WindowType::Normal: + case WindowType::ToolWindow: + case WindowType::Tooltip: + return false; + default: + return true; + } + } + + void did_switch_window_stack(Badge, WindowStack&, WindowStack&); + + template + IterationDecision for_each_visible_window_from_back_to_front(Callback, WindowStack* = nullptr); + template + IterationDecision for_each_visible_window_from_front_to_back(Callback, WindowStack* = nullptr); MultiScaleBitmaps const* overlay_rect_shadow() const { return m_overlay_rect_shadow.ptr(); } private: RefPtr get_cursor(String const& name); + void notify_new_active_window(Window&); + void notify_new_active_input_window(Window&); + void notify_previous_active_window(Window&); + void notify_previous_active_input_window(Window&); + void process_mouse_event(MouseEvent&); void process_event_for_doubleclick(Window& window, MouseEvent& event); bool process_ongoing_window_resize(MouseEvent const&); @@ -260,6 +328,8 @@ private: void do_move_to_front(Window&, bool, bool); + [[nodiscard]] static WindowStack& get_rendering_window_stacks(WindowStack*&); + RefPtr m_hidden_cursor; RefPtr m_arrow_cursor; RefPtr m_hand_cursor; @@ -279,7 +349,9 @@ private: RefPtr m_overlay_rect_shadow; - WindowStack m_window_stack; + // Setup 2 rows 1 column by default + NonnullOwnPtrVector, 2> m_window_stacks; + WindowStack* m_current_window_stack { nullptr }; struct DoubleClickInfo { struct ClickMetadata { @@ -317,8 +389,7 @@ private: bool m_previous_event_was_super_keydown { false }; WeakPtr m_hovered_window; - WeakPtr m_active_input_window; - WeakPtr m_active_input_tracking_window; + WeakPtr m_highlight_window; WeakPtr m_window_with_active_menu; OwnPtr m_geometry_overlay; @@ -349,8 +420,85 @@ private: String m_dnd_text; RefPtr m_dnd_mime_data; + + WindowStack* m_switching_to_window_stack { nullptr }; + Vector, 4> m_carry_window_to_new_stack; }; +template +inline IterationDecision WindowManager::for_each_visible_window_from_back_to_front(Callback callback, WindowStack* specific_stack) +{ + auto* window_stack = specific_stack; + WindowStack* transitioning_to_window_stack = nullptr; + if (!window_stack) + window_stack = &get_rendering_window_stacks(transitioning_to_window_stack); + auto for_each_window = [&]() { + if constexpr (is_stationary_window_type(window_type)) { + auto& stationary_stack = window_stack->stationary_window_stack(); + return stationary_stack.for_each_visible_window_of_type_from_back_to_front(window_type, callback); + } else { + auto decision = window_stack->for_each_visible_window_of_type_from_back_to_front(window_type, callback); + if (decision == IterationDecision::Continue && transitioning_to_window_stack) + decision = transitioning_to_window_stack->for_each_visible_window_of_type_from_back_to_front(window_type, callback); + return decision; + } + }; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + return for_each_window.template operator()(); +} + +template +inline IterationDecision WindowManager::for_each_visible_window_from_front_to_back(Callback callback, WindowStack* specific_stack) +{ + auto* window_stack = specific_stack; + WindowStack* transitioning_to_window_stack = nullptr; + if (!window_stack) + window_stack = &get_rendering_window_stacks(transitioning_to_window_stack); + auto for_each_window = [&]() { + if constexpr (is_stationary_window_type(window_type)) { + auto& stationary_stack = window_stack->stationary_window_stack(); + return stationary_stack.for_each_visible_window_of_type_from_front_to_back(window_type, callback); + } else { + auto decision = window_stack->for_each_visible_window_of_type_from_front_to_back(window_type, callback); + if (decision == IterationDecision::Continue && transitioning_to_window_stack) + decision = transitioning_to_window_stack->for_each_visible_window_of_type_from_front_to_back(window_type, callback); + return decision; + } + }; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + if (for_each_window.template operator()() == IterationDecision::Break) + return IterationDecision::Break; + return for_each_window.template operator()(); +} + template void WindowManager::for_each_window_manager(Callback callback) { diff --git a/Userland/Services/WindowServer/WindowStack.cpp b/Userland/Services/WindowServer/WindowStack.cpp index 3b914f2b8b5..f10a9641b2d 100644 --- a/Userland/Services/WindowServer/WindowStack.cpp +++ b/Userland/Services/WindowServer/WindowStack.cpp @@ -5,10 +5,13 @@ */ #include "WindowStack.h" +#include "WindowManager.h" namespace WindowServer { -WindowStack::WindowStack() +WindowStack::WindowStack(unsigned row, unsigned column) + : m_row(row) + , m_column(column) { } @@ -28,6 +31,12 @@ void WindowStack::remove(Window& window) VERIFY(window.outer_stack() == this); m_windows.remove(window); window.set_outer_stack({}, nullptr); + if (m_active_window == &window) + m_active_window = nullptr; + if (m_active_input_window == &window) + m_active_input_window = nullptr; + if (m_active_input_tracking_window == &window) + m_active_input_tracking_window = nullptr; } void WindowStack::move_to_front(Window& window) @@ -48,12 +57,11 @@ Window* WindowStack::window_at(Gfx::IntPoint const& position, IncludeWindowFrame return result->window; } -void WindowStack::set_highlight_window(Window* window) +Window* WindowStack::highlight_window() const { - if (!window) - m_highlight_window = nullptr; - else - m_highlight_window = window->make_weak_ptr(); + if (auto* window = WindowManager::the().highlight_window(); window && window->outer_stack() == this) + return window; + return nullptr; } void WindowStack::set_active_window(Window* window) @@ -64,15 +72,24 @@ void WindowStack::set_active_window(Window* window) m_active_window = window->make_weak_ptr(); } +void WindowStack::set_all_occluded(bool occluded) +{ + for (auto& window : m_windows) { + if (!WindowManager::is_stationary_window_type(window.type())) + window.set_occluded(occluded); + } +} + Optional WindowStack::hit_test(Gfx::IntPoint const& position) const { Optional result; - const_cast(this)->for_each_visible_window_from_front_to_back([&](Window& window) { + WindowManager::the().for_each_visible_window_from_front_to_back([&](Window& window) { result = window.hit_test(position); if (result.has_value()) return IterationDecision::Break; return IterationDecision::Continue; - }); + }, + const_cast(this)); return result; } diff --git a/Userland/Services/WindowServer/WindowStack.h b/Userland/Services/WindowServer/WindowStack.h index ccabb8bb6a6..a4aaec944ea 100644 --- a/Userland/Services/WindowServer/WindowStack.h +++ b/Userland/Services/WindowServer/WindowStack.h @@ -10,9 +10,11 @@ namespace WindowServer { +class Compositor; + class WindowStack { public: - WindowStack(); + WindowStack(unsigned row, unsigned column); ~WindowStack(); bool is_empty() const { return m_windows.is_empty(); } @@ -25,11 +27,8 @@ public: No, }; Window* window_at(Gfx::IntPoint const&, IncludeWindowFrame = IncludeWindowFrame::Yes) const; + Window* highlight_window() const; - template - IterationDecision for_each_visible_window_from_back_to_front(Callback); - template - IterationDecision for_each_visible_window_from_front_to_back(Callback); template IterationDecision for_each_visible_window_of_type_from_front_to_back(WindowType, Callback, bool ignore_highlight = false); template @@ -42,26 +41,51 @@ public: Window::List& windows() { return m_windows; } - Window* highlight_window() { return m_highlight_window; } - Window const* highlight_window() const { return m_highlight_window; } - void set_highlight_window(Window*); - Window* active_window() { return m_active_window; } Window const* active_window() const { return m_active_window; } void set_active_window(Window*); + Window* active_input_window() { return m_active_input_window; } + Window const* active_input_window() const { return m_active_input_window; } + void set_active_input_window(Window* window) { m_active_input_window = window; } + + Window* active_input_tracking_window() { return m_active_input_tracking_window; } + Window const* active_input_tracking_window() const { return m_active_input_tracking_window; } + void set_active_input_tracking_window(Window* window) { m_active_input_tracking_window = window; } + Optional hit_test(Gfx::IntPoint const&) const; + unsigned row() const { return m_row; } + unsigned column() const { return m_column; } + + void set_transition_offset(Badge, Gfx::IntPoint const& transition_offset) { m_transition_offset = transition_offset; } + Gfx::IntPoint const& transition_offset() const { return m_transition_offset; } + + void set_stationary_window_stack(WindowStack& window_stack) { m_stationary_window_stack = &window_stack; } + WindowStack& stationary_window_stack() + { + VERIFY(m_stationary_window_stack); + return *m_stationary_window_stack; + } + + void set_all_occluded(bool); + private: - WeakPtr m_highlight_window; WeakPtr m_active_window; + WeakPtr m_active_input_window; + WeakPtr m_active_input_tracking_window; Window::List m_windows; + unsigned m_row { 0 }; + unsigned m_column { 0 }; + Gfx::IntPoint m_transition_offset; + WindowStack* m_stationary_window_stack { nullptr }; }; template inline IterationDecision WindowStack::for_each_visible_window_of_type_from_back_to_front(WindowType type, Callback callback, bool ignore_highlight) { + auto* highlight_window = this->highlight_window(); bool do_highlight_window_at_end = false; for (auto& window : m_windows) { if (!window.is_visible()) @@ -70,7 +94,7 @@ inline IterationDecision WindowStack::for_each_visible_window_of_type_from_back_ continue; if (window.type() != type) continue; - if (!ignore_highlight && m_highlight_window == &window) { + if (!ignore_highlight && highlight_window == &window) { do_highlight_window_at_end = true; continue; } @@ -78,7 +102,7 @@ inline IterationDecision WindowStack::for_each_visible_window_of_type_from_back_ return IterationDecision::Break; } if (do_highlight_window_at_end) { - if (callback(*m_highlight_window) == IterationDecision::Break) + if (callback(*highlight_window) == IterationDecision::Break) return IterationDecision::Break; } return IterationDecision::Continue; @@ -87,8 +111,9 @@ inline IterationDecision WindowStack::for_each_visible_window_of_type_from_back_ template inline IterationDecision WindowStack::for_each_visible_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight) { - if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) { - if (callback(*m_highlight_window) == IterationDecision::Break) + auto* highlight_window = this->highlight_window(); + if (!ignore_highlight && highlight_window && highlight_window->type() == type && highlight_window->is_visible()) { + if (callback(*highlight_window) == IterationDecision::Break) return IterationDecision::Break; } @@ -101,7 +126,7 @@ inline IterationDecision WindowStack::for_each_visible_window_of_type_from_front continue; if (window.type() != type) continue; - if (!ignore_highlight && &window == m_highlight_window) + if (!ignore_highlight && &window == highlight_window) continue; if (callback(window) == IterationDecision::Break) return IterationDecision::Break; @@ -120,55 +145,12 @@ inline void WindowStack::for_each_window(Callback callback) } } -template -inline IterationDecision WindowStack::for_each_visible_window_from_back_to_front(Callback callback) -{ - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Desktop, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Normal, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::ToolWindow, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Taskbar, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::AppletArea, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Notification, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Tooltip, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_back_to_front(WindowType::Menu, callback) == IterationDecision::Break) - return IterationDecision::Break; - return for_each_visible_window_of_type_from_back_to_front(WindowType::WindowSwitcher, callback); -} - -template -inline IterationDecision WindowStack::for_each_visible_window_from_front_to_back(Callback callback) -{ - if (for_each_visible_window_of_type_from_front_to_back(WindowType::WindowSwitcher, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::Menu, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::Tooltip, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::Notification, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::AppletArea, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::Taskbar, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::ToolWindow, callback) == IterationDecision::Break) - return IterationDecision::Break; - if (for_each_visible_window_of_type_from_front_to_back(WindowType::Normal, callback) == IterationDecision::Break) - return IterationDecision::Break; - return for_each_visible_window_of_type_from_front_to_back(WindowType::Desktop, callback); -} - template inline IterationDecision WindowStack::for_each_window_of_type_from_front_to_back(WindowType type, Callback callback, bool ignore_highlight) { - if (!ignore_highlight && m_highlight_window && m_highlight_window->type() == type && m_highlight_window->is_visible()) { - if (callback(*m_highlight_window) == IterationDecision::Break) + auto* highlight_window = this->highlight_window(); + if (!ignore_highlight && highlight_window && highlight_window->type() == type && highlight_window->is_visible()) { + if (callback(*highlight_window) == IterationDecision::Break) return IterationDecision::Break; } @@ -177,7 +159,7 @@ inline IterationDecision WindowStack::for_each_window_of_type_from_front_to_back auto& window = *reverse_iterator; if (window.type() != type) continue; - if (!ignore_highlight && &window == m_highlight_window) + if (!ignore_highlight && &window == highlight_window) continue; if (callback(window) == IterationDecision::Break) return IterationDecision::Break; diff --git a/Userland/Services/WindowServer/WindowSwitcher.cpp b/Userland/Services/WindowServer/WindowSwitcher.cpp index ecc75a9bce1..2ba8aafcbd3 100644 --- a/Userland/Services/WindowServer/WindowSwitcher.cpp +++ b/Userland/Services/WindowServer/WindowSwitcher.cpp @@ -202,18 +202,21 @@ void WindowSwitcher::refresh() m_selected_index = 0; int window_count = 0; int longest_title_width = 0; - wm.window_stack().for_each_window_of_type_from_front_to_back( - WindowType::Normal, [&](Window& window) { - if (window.is_frameless()) + wm.for_each_window_stack([&](auto& window_stack) { + window_stack.for_each_window_of_type_from_front_to_back( + WindowType::Normal, [&](Window& window) { + if (window.is_frameless()) + return IterationDecision::Continue; + ++window_count; + longest_title_width = max(longest_title_width, wm.font().width(window.computed_title())); + if (selected_window == &window) + m_selected_index = m_windows.size(); + m_windows.append(window); return IterationDecision::Continue; - ++window_count; - longest_title_width = max(longest_title_width, wm.font().width(window.computed_title())); - if (selected_window == &window) - m_selected_index = m_windows.size(); - m_windows.append(window); - return IterationDecision::Continue; - }, - true); + }, + true); + return IterationDecision::Continue; + }); if (m_windows.is_empty()) { hide(); return;