瀏覽代碼

WindowServer+LibGfx: Show menus in windows! :^)

This patch begins the transition away from the global menu towards
per-window menus instead.

The global menu looks neat, but has always felt clunky, and there
are a number of usability problems with it, especially in programs
with multiple windows.

You can now call GUI::Window::set_menubar() to add a menubar to
your window. It will be specific to that one window only.
Andreas Kling 4 年之前
父節點
當前提交
e76771bfad

+ 2 - 2
Userland/Applications/ThemeEditor/PreviewWidget.cpp

@@ -145,10 +145,10 @@ void PreviewWidget::paint_event(GUI::PaintEvent& event)
             button.rect = rect;
         }
 
-        auto frame_rect = Gfx::WindowTheme::current().frame_rect_for_window(Gfx::WindowTheme::WindowType::Normal, rect, m_preview_palette);
+        auto frame_rect = Gfx::WindowTheme::current().frame_rect_for_window(Gfx::WindowTheme::WindowType::Normal, rect, m_preview_palette, 0);
         Gfx::PainterStateSaver saver(painter);
         painter.translate(frame_rect.location());
-        Gfx::WindowTheme::current().paint_normal_frame(painter, state, rect, title, icon, m_preview_palette, buttons.last().rect);
+        Gfx::WindowTheme::current().paint_normal_frame(painter, state, rect, title, icon, m_preview_palette, buttons.last().rect, 0);
 
         for (auto& button : buttons) {
             Gfx::StylePainter::paint_button(painter, button.rect, m_preview_palette, Gfx::ButtonStyle::Normal, false);

+ 1 - 1
Userland/Demos/LibGfxScaleDemo/main.cpp

@@ -93,7 +93,7 @@ void Canvas::paint_event(GUI::PaintEvent& event)
 void Canvas::draw(Gfx::Painter& painter)
 {
     auto active_window_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window.png");
-    Gfx::WindowTheme::current().paint_normal_frame(painter, Gfx::WindowTheme::WindowState::Active, { 4, 18, WIDTH - 8, HEIGHT - 29 }, "Well hello friends 🐞", *active_window_icon, palette(), { WIDTH - 20, 6, 16, 16 });
+    Gfx::WindowTheme::current().paint_normal_frame(painter, Gfx::WindowTheme::WindowState::Active, { 4, 18, WIDTH - 8, HEIGHT - 29 }, "Well hello friends 🐞", *active_window_icon, palette(), { WIDTH - 20, 6, 16, 16 }, 0);
 
     painter.draw_rect({ 20, 34, WIDTH - 40, HEIGHT - 45 }, palette().color(Gfx::ColorRole::Selection), true);
     painter.draw_rect({ 24, 38, WIDTH - 48, HEIGHT - 53 }, palette().color(Gfx::ColorRole::Selection));

+ 10 - 0
Userland/Libraries/LibGUI/Window.cpp

@@ -35,6 +35,7 @@
 #include <LibGUI/Application.h>
 #include <LibGUI/Desktop.h>
 #include <LibGUI/Event.h>
+#include <LibGUI/MenuBar.h>
 #include <LibGUI/Painter.h>
 #include <LibGUI/Widget.h>
 #include <LibGUI/Window.h>
@@ -1052,4 +1053,13 @@ Gfx::Bitmap* Window::back_bitmap()
     return m_back_store ? &m_back_store->bitmap() : nullptr;
 }
 
+void Window::set_menubar(RefPtr<MenuBar> menubar)
+{
+    if (m_menubar == menubar)
+        return;
+    m_menubar = move(menubar);
+    if (m_window_id && m_menubar)
+        WindowServerConnection::the().send_sync<Messages::WindowServer::SetWindowMenubar>(m_window_id, m_menubar->menubar_id());
+}
+
 }

+ 4 - 0
Userland/Libraries/LibGUI/Window.h

@@ -211,6 +211,8 @@ public:
 
     void did_disable_focused_widget(Badge<Widget>);
 
+    void set_menubar(RefPtr<MenuBar>);
+
 protected:
     Window(Core::Object* parent = nullptr);
     virtual void wm_event(WMEvent&);
@@ -241,6 +243,8 @@ private:
     OwnPtr<WindowBackingStore> m_front_store;
     OwnPtr<WindowBackingStore> m_back_store;
 
+    RefPtr<MenuBar> m_menubar;
+
     RefPtr<Gfx::Bitmap> m_icon;
     RefPtr<Gfx::Bitmap> m_custom_cursor;
     int m_window_id { 0 };

+ 16 - 7
Userland/Libraries/LibGfx/ClassicWindowTheme.cpp

@@ -33,6 +33,8 @@
 
 namespace Gfx {
 
+static constexpr int menu_bar_height = 19;
+
 ClassicWindowTheme::ClassicWindowTheme()
 {
 }
@@ -70,9 +72,9 @@ Gfx::IntRect ClassicWindowTheme::title_bar_text_rect(WindowType window_type, con
     };
 }
 
-void ClassicWindowTheme::paint_normal_frame(Painter& painter, WindowState window_state, const IntRect& window_rect, const StringView& title_text, const Bitmap& icon, const Palette& palette, const IntRect& leftmost_button_rect) const
+void ClassicWindowTheme::paint_normal_frame(Painter& painter, WindowState window_state, const IntRect& window_rect, const StringView& title_text, const Bitmap& icon, const Palette& palette, const IntRect& leftmost_button_rect, int menu_row_count) const
 {
-    auto frame_rect = frame_rect_for_window(WindowType::Normal, window_rect, palette);
+    auto frame_rect = frame_rect_for_window(WindowType::Normal, window_rect, palette, menu_row_count);
     frame_rect.set_location({ 0, 0 });
     Gfx::StylePainter::paint_window_frame(painter, frame_rect, palette);
 
@@ -114,7 +116,7 @@ void ClassicWindowTheme::paint_normal_frame(Painter& painter, WindowState window
 
 void ClassicWindowTheme::paint_tool_window_frame(Painter& painter, WindowState window_state, const IntRect& window_rect, const StringView& title_text, const Palette& palette, const IntRect& leftmost_button_rect) const
 {
-    auto frame_rect = frame_rect_for_window(WindowType::ToolWindow, window_rect, palette);
+    auto frame_rect = frame_rect_for_window(WindowType::ToolWindow, window_rect, palette, 0);
     frame_rect.set_location({ 0, 0 });
     Gfx::StylePainter::paint_window_frame(painter, frame_rect, palette);
 
@@ -143,6 +145,13 @@ void ClassicWindowTheme::paint_tool_window_frame(Painter& painter, WindowState w
     }
 }
 
+IntRect ClassicWindowTheme::menu_bar_rect(WindowType window_type, const IntRect& window_rect, const Palette& palette, int menu_row_count) const
+{
+    if (window_type != WindowType::Normal)
+        return {};
+    return { 4, 4 + title_bar_height(window_type, palette) + 2, window_rect.width(), menu_bar_height * menu_row_count };
+}
+
 IntRect ClassicWindowTheme::title_bar_rect(WindowType window_type, const IntRect& window_rect, const Palette& palette) const
 {
     auto& title_font = FontDatabase::default_bold_font();
@@ -173,7 +182,7 @@ ClassicWindowTheme::FrameColors ClassicWindowTheme::compute_frame_colors(WindowS
 
 void ClassicWindowTheme::paint_notification_frame(Painter& painter, const IntRect& window_rect, const Palette& palette, const IntRect& close_button_rect) const
 {
-    auto frame_rect = frame_rect_for_window(WindowType::Notification, window_rect, palette);
+    auto frame_rect = frame_rect_for_window(WindowType::Notification, window_rect, palette, 0);
     frame_rect.set_location({ 0, 0 });
     Gfx::StylePainter::paint_window_frame(painter, frame_rect, palette);
 
@@ -191,7 +200,7 @@ void ClassicWindowTheme::paint_notification_frame(Painter& painter, const IntRec
     }
 }
 
-IntRect ClassicWindowTheme::frame_rect_for_window(WindowType window_type, const IntRect& window_rect, const Gfx::Palette& palette) const
+IntRect ClassicWindowTheme::frame_rect_for_window(WindowType window_type, const IntRect& window_rect, const Gfx::Palette& palette, int menu_row_count) const
 {
     auto window_titlebar_height = title_bar_height(window_type, palette);
 
@@ -200,9 +209,9 @@ IntRect ClassicWindowTheme::frame_rect_for_window(WindowType window_type, const
     case WindowType::ToolWindow:
         return {
             window_rect.x() - 4,
-            window_rect.y() - window_titlebar_height - 6,
+            window_rect.y() - window_titlebar_height - 6 - menu_row_count * menu_bar_height,
             window_rect.width() + 8,
-            window_rect.height() + 10 + window_titlebar_height
+            window_rect.height() + 10 + window_titlebar_height + menu_row_count * menu_bar_height
         };
     case WindowType::Notification:
         return {

+ 4 - 2
Userland/Libraries/LibGfx/ClassicWindowTheme.h

@@ -36,7 +36,7 @@ public:
     ClassicWindowTheme();
     virtual ~ClassicWindowTheme() override;
 
-    virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect) const override;
+    virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect, int menu_row_count) const override;
     virtual void paint_tool_window_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Palette&, const IntRect& leftmost_button_rect) const override;
     virtual void paint_notification_frame(Painter&, const IntRect& window_rect, const Palette&, const IntRect& close_button_rect) const override;
 
@@ -45,7 +45,9 @@ public:
     virtual IntRect title_bar_icon_rect(WindowType, const IntRect& window_rect, const Palette&) const override;
     virtual IntRect title_bar_text_rect(WindowType, const IntRect& window_rect, const Palette&) const override;
 
-    virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&) const override;
+    virtual IntRect menu_bar_rect(WindowType, const IntRect& window_rect, const Palette&, int menu_row_count) const override;
+
+    virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&, int menu_row_count) const override;
 
     virtual Vector<IntRect> layout_buttons(WindowType, const IntRect& window_rect, const Palette&, size_t buttons) const override;
     virtual bool is_simple_rect_frame() const override { return true; }

+ 4 - 2
Userland/Libraries/LibGfx/WindowTheme.h

@@ -51,7 +51,7 @@ public:
 
     static WindowTheme& current();
 
-    virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect) const = 0;
+    virtual void paint_normal_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Bitmap& icon, const Palette&, const IntRect& leftmost_button_rect, int menu_row_count) const = 0;
     virtual void paint_tool_window_frame(Painter&, WindowState, const IntRect& window_rect, const StringView& title, const Palette&, const IntRect& leftmost_button_rect) const = 0;
     virtual void paint_notification_frame(Painter&, const IntRect& window_rect, const Palette&, const IntRect& close_button_rect) const = 0;
 
@@ -60,7 +60,9 @@ public:
     virtual IntRect title_bar_icon_rect(WindowType, const IntRect& window_rect, const Palette&) const = 0;
     virtual IntRect title_bar_text_rect(WindowType, const IntRect& window_rect, const Palette&) const = 0;
 
-    virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&) const = 0;
+    virtual IntRect menu_bar_rect(WindowType, const IntRect& window_rect, const Palette&, int menu_row_count) const = 0;
+
+    virtual IntRect frame_rect_for_window(WindowType, const IntRect& window_rect, const Palette&, int menu_row_count) const = 0;
 
     virtual Vector<IntRect> layout_buttons(WindowType, const IntRect& window_rect, const Palette&, size_t buttons) const = 0;
     virtual bool is_simple_rect_frame() const = 0;

+ 25 - 1
Userland/Services/WindowServer/ClientConnection.cpp

@@ -104,7 +104,7 @@ void ClientConnection::notify_about_new_screen_rect(const Gfx::IntRect& rect)
 OwnPtr<Messages::WindowServer::CreateMenubarResponse> ClientConnection::handle(const Messages::WindowServer::CreateMenubar&)
 {
     int menubar_id = m_next_menubar_id++;
-    auto menubar = make<MenuBar>(*this, menubar_id);
+    auto menubar = MenuBar::create(*this, menubar_id);
     m_menubars.set(menubar_id, move(menubar));
     return make<Messages::WindowServer::CreateMenubarResponse>(menubar_id);
 }
@@ -160,6 +160,30 @@ OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> ClientConnection::
     return make<Messages::WindowServer::SetApplicationMenubarResponse>();
 }
 
+OwnPtr<Messages::WindowServer::SetWindowMenubarResponse> ClientConnection::handle(const Messages::WindowServer::SetWindowMenubar& message)
+{
+    RefPtr<Window> window;
+    {
+        auto it = m_windows.find(message.window_id());
+        if (it == m_windows.end()) {
+            did_misbehave("SetWindowMenubar: Bad window ID");
+            return {};
+        }
+        window = it->value;
+    }
+    RefPtr<MenuBar> menubar;
+    if (message.menubar_id() != -1) {
+        auto it = m_menubars.find(message.menubar_id());
+        if (it == m_menubars.end()) {
+            did_misbehave("SetWindowMenubar: Bad menubar ID");
+            return {};
+        }
+        menubar = *(*it).value;
+    }
+    window->set_menubar(menubar);
+    return make<Messages::WindowServer::SetWindowMenubarResponse>();
+}
+
 OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> ClientConnection::handle(const Messages::WindowServer::AddMenuToMenubar& message)
 {
     int menubar_id = message.menubar_id();

+ 11 - 1
Userland/Services/WindowServer/ClientConnection.h

@@ -78,6 +78,15 @@ public:
         return menu.value().ptr();
     }
 
+    template<typename Callback>
+    void for_each_window(Callback callback)
+    {
+        for (auto& it : m_windows) {
+            if (callback(*it.value) == IterationDecision::Break)
+                break;
+        }
+    }
+
     void notify_display_link(Badge<Compositor>);
 
 private:
@@ -98,6 +107,7 @@ private:
     virtual OwnPtr<Messages::WindowServer::DestroyMenuResponse> handle(const Messages::WindowServer::DestroyMenu&) override;
     virtual OwnPtr<Messages::WindowServer::AddMenuToMenubarResponse> handle(const Messages::WindowServer::AddMenuToMenubar&) override;
     virtual OwnPtr<Messages::WindowServer::SetApplicationMenubarResponse> handle(const Messages::WindowServer::SetApplicationMenubar&) override;
+    virtual OwnPtr<Messages::WindowServer::SetWindowMenubarResponse> handle(const Messages::WindowServer::SetWindowMenubar&) override;
     virtual OwnPtr<Messages::WindowServer::AddMenuItemResponse> handle(const Messages::WindowServer::AddMenuItem&) override;
     virtual OwnPtr<Messages::WindowServer::AddMenuSeparatorResponse> handle(const Messages::WindowServer::AddMenuSeparator&) override;
     virtual OwnPtr<Messages::WindowServer::UpdateMenuItemResponse> handle(const Messages::WindowServer::UpdateMenuItem&) override;
@@ -157,7 +167,7 @@ private:
     Window* window_from_id(i32 window_id);
 
     HashMap<int, NonnullRefPtr<Window>> m_windows;
-    HashMap<int, NonnullOwnPtr<MenuBar>> m_menubars;
+    HashMap<int, NonnullRefPtr<MenuBar>> m_menubars;
     HashMap<int, NonnullRefPtr<Menu>> m_menus;
     WeakPtr<MenuBar> m_app_menubar;
 

+ 1 - 1
Userland/Services/WindowServer/Menu.cpp

@@ -122,7 +122,7 @@ int Menu::content_width() const
     if (widest_shortcut)
         widest_item += padding_between_text_and_shortcut() + widest_shortcut;
 
-    return max(widest_item, rect_in_menubar().width()) + horizontal_padding() + frame_thickness() * 2;
+    return max(widest_item, rect_in_global_menubar().width()) + horizontal_padding() + frame_thickness() * 2;
 }
 
 void Menu::redraw()

+ 12 - 6
Userland/Services/WindowServer/Menu.h

@@ -74,11 +74,15 @@ public:
             callback(item);
     }
 
-    Gfx::IntRect text_rect_in_menubar() const { return m_text_rect_in_menubar; }
-    void set_text_rect_in_menubar(const Gfx::IntRect& rect) { m_text_rect_in_menubar = rect; }
+    Gfx::IntRect text_rect_in_global_menubar() const { return m_text_rect_in_global_menubar; }
+    void set_text_rect_in_global_menubar(const Gfx::IntRect& rect) { m_text_rect_in_global_menubar = rect; }
+    Gfx::IntRect rect_in_global_menubar() const { return m_rect_in_global_menubar; }
+    void set_rect_in_global_menubar(const Gfx::IntRect& rect) { m_rect_in_global_menubar = rect; }
 
-    Gfx::IntRect rect_in_menubar() const { return m_rect_in_menubar; }
-    void set_rect_in_menubar(const Gfx::IntRect& rect) { m_rect_in_menubar = rect; }
+    Gfx::IntRect text_rect_in_window_menubar() const { return m_text_rect_in_window_menubar; }
+    void set_text_rect_in_window_menubar(const Gfx::IntRect& rect) { m_text_rect_in_window_menubar = rect; }
+    Gfx::IntRect rect_in_window_menubar() const { return m_rect_in_window_menubar; }
+    void set_rect_in_window_menubar(const Gfx::IntRect& rect) { m_rect_in_window_menubar = rect; }
 
     Window* menu_window() { return m_menu_window.ptr(); }
     Window& ensure_menu_window();
@@ -149,8 +153,10 @@ private:
     ClientConnection* m_client { nullptr };
     int m_menu_id { 0 };
     String m_name;
-    Gfx::IntRect m_rect_in_menubar;
-    Gfx::IntRect m_text_rect_in_menubar;
+    Gfx::IntRect m_rect_in_global_menubar;
+    Gfx::IntRect m_text_rect_in_global_menubar;
+    Gfx::IntRect m_rect_in_window_menubar;
+    Gfx::IntRect m_text_rect_in_window_menubar;
     MenuBar* m_menubar { nullptr };
     NonnullOwnPtrVector<MenuItem> m_items;
     RefPtr<Window> m_menu_window;

+ 6 - 2
Userland/Services/WindowServer/MenuBar.h

@@ -33,9 +33,11 @@
 
 namespace WindowServer {
 
-class MenuBar : public Weakable<MenuBar> {
+class MenuBar
+    : public RefCounted<MenuBar>
+    , public Weakable<MenuBar> {
 public:
-    MenuBar(ClientConnection& client, int menubar_id);
+    static NonnullRefPtr<MenuBar> create(ClientConnection& client, int menubar_id) { return adopt(*new MenuBar(client, menubar_id)); }
     ~MenuBar();
 
     ClientConnection& client() { return m_client; }
@@ -53,6 +55,8 @@ public:
     }
 
 private:
+    MenuBar(ClientConnection&, int menubar_id);
+
     ClientConnection& m_client;
     int m_menubar_id { 0 };
     Vector<Menu*> m_menus;

+ 34 - 9
Userland/Services/WindowServer/MenuManager.cpp

@@ -32,6 +32,7 @@
 #include <LibGfx/Font.h>
 #include <LibGfx/Painter.h>
 #include <WindowServer/AppletManager.h>
+#include <WindowServer/ClientConnection.h>
 #include <WindowServer/MenuManager.h>
 #include <WindowServer/Screen.h>
 #include <WindowServer/WindowManager.h>
@@ -96,19 +97,20 @@ void MenuManager::draw()
     painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, palette.threed_shadow2());
 
     for_each_active_menubar_menu([&](Menu& menu) {
+        auto text_rect = menu.text_rect_in_global_menubar();
         Color text_color = palette.window_text();
         if (is_open(menu) && &menu == m_current_menu_bar_menu) {
-            painter.fill_rect(menu.rect_in_menubar(), palette.menu_selection());
-            painter.draw_rect(menu.rect_in_menubar(), palette.menu_selection().darkened());
+            painter.fill_rect(menu.rect_in_global_menubar(), palette.menu_selection());
+            painter.draw_rect(menu.rect_in_global_menubar(), palette.menu_selection().darkened());
             text_color = palette.menu_selection_text();
+            text_rect.move_by(1, 1);
         }
         painter.draw_text(
-            menu.text_rect_in_menubar(),
+            text_rect,
             menu.name(),
             menu.title_font(),
             Gfx::TextAlignment::CenterLeft,
             text_color);
-        //painter.draw_rect(menu.text_rect_in_menubar(), Color::Magenta);
         return IterationDecision::Continue;
     });
 
@@ -216,7 +218,7 @@ void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
     auto* active_window = WindowManager::the().active_window();
     bool handled_menubar_event = false;
     for_each_active_menubar_menu([&](Menu& menu) {
-        if (menu.rect_in_menubar().contains(mouse_event.position())) {
+        if (menu.rect_in_global_menubar().contains(mouse_event.position())) {
             handled_menubar_event = &menu == m_system_menu || !active_window || !active_window->is_modal();
             if (handled_menubar_event)
                 handle_menu_mouse_event(menu, mouse_event);
@@ -383,6 +385,25 @@ void MenuManager::close_menu_and_descendants(Menu& menu)
     close_menus(menus_to_close);
 }
 
+void MenuManager::set_hovered_menu(Menu* menu)
+{
+    if (m_hovered_menu == menu)
+        return;
+    if (menu) {
+        m_hovered_menu = menu->make_weak_ptr<Menu>();
+    } else {
+        // FIXME: This is quite aggressive. If we knew which window the previously hovered menu was in,
+        //        we could just invalidate that one instead of iterating all windows in the client.
+        if (auto* client = m_hovered_menu->client()) {
+            client->for_each_window([&](Window& window) {
+                window.invalidate_menubar();
+                return IterationDecision::Continue;
+            });
+        }
+        m_hovered_menu = nullptr;
+    }
+}
+
 void MenuManager::open_menu(Menu& menu, bool from_menu_bar, bool as_current_menu)
 {
     if (from_menu_bar)
@@ -405,7 +426,7 @@ void MenuManager::open_menu(Menu& menu, bool from_menu_bar, bool as_current_menu
             menu.ensure_menu_window();
         }
         if (should_update_position)
-            menu.menu_window()->move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 });
+            menu.menu_window()->move_to({ menu.rect_in_global_menubar().x(), menu.rect_in_global_menubar().bottom() + 2 });
         menu.menu_window()->set_visible(true);
     }
 
@@ -430,6 +451,10 @@ void MenuManager::clear_current_menu()
         // When closing the last menu, restore the previous active input window
         auto& wm = WindowManager::the();
         wm.restore_active_input_window(m_previous_input_window);
+        if (auto* window = wm.window_with_active_menu()) {
+            window->invalidate_menubar();
+        }
+        wm.set_window_with_active_menu(nullptr);
     }
 }
 
@@ -485,12 +510,12 @@ void MenuManager::set_current_menubar(MenuBar* menubar)
     Gfx::IntPoint next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 };
     for_each_active_menubar_menu([&](Menu& menu) {
         int text_width = menu.title_font().width(menu.name());
-        menu.set_rect_in_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect().height() - 1 });
+        menu.set_rect_in_global_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect().height() - 1 });
 
         Gfx::IntRect text_rect { next_menu_location.translated(0, 1), { text_width, menubar_rect().height() - 3 } };
 
-        menu.set_text_rect_in_menubar(text_rect);
-        next_menu_location.move_by(menu.rect_in_menubar().width(), 0);
+        menu.set_text_rect_in_global_menubar(text_rect);
+        next_menu_location.move_by(menu.rect_in_global_menubar().width(), 0);
         return IterationDecision::Continue;
     });
     refresh();

+ 5 - 1
Userland/Services/WindowServer/MenuManager.h

@@ -50,7 +50,7 @@ public:
     bool has_open_menu() const { return !m_open_menu_stack.is_empty(); }
 
     Gfx::IntRect menubar_rect() const;
-    static int menubar_menu_margin() { return 16; }
+    static int menubar_menu_margin() { return 13; }
 
     void set_needs_window_resize();
 
@@ -93,6 +93,9 @@ public:
 
     void did_change_theme();
 
+    void set_hovered_menu(Menu*);
+    Menu* hovered_menu() { return m_hovered_menu; }
+
 private:
     void close_menus(const Vector<Menu*>&);
 
@@ -121,6 +124,7 @@ private:
     int m_theme_index { 0 };
 
     WeakPtr<MenuBar> m_current_menubar;
+    WeakPtr<Menu> m_hovered_menu;
 };
 
 }

+ 32 - 0
Userland/Services/WindowServer/Window.cpp

@@ -942,4 +942,36 @@ bool Window::hit_test(const Gfx::IntPoint& point, bool include_frame) const
     return color.alpha() >= threshold;
 }
 
+void Window::set_menubar(MenuBar* menubar)
+{
+    if (m_menubar == menubar)
+        return;
+    m_menubar = menubar;
+    if (m_menubar) {
+        auto& wm = WindowManager::the();
+        Gfx::IntPoint next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 };
+        auto menubar_rect = Gfx::WindowTheme::current().menu_bar_rect(Gfx::WindowTheme::WindowType::Normal, rect(), wm.palette(), 1);
+        m_menubar->for_each_menu([&](Menu& menu) {
+            int text_width = wm.font().width(menu.name());
+            menu.set_rect_in_window_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect.height() });
+
+            Gfx::IntRect text_rect { next_menu_location.translated(0, 1), { text_width, menubar_rect.height() - 3 } };
+
+            menu.set_text_rect_in_window_menubar(text_rect);
+            next_menu_location.move_by(menu.rect_in_window_menubar().width(), 0);
+            return IterationDecision::Continue;
+        });
+    }
+    Compositor::the().invalidate_occlusions();
+    frame().invalidate();
+}
+
+void Window::invalidate_menubar()
+{
+    if (!menubar())
+        return;
+    // FIXME: This invalidates way more than the menubar!
+    frame().invalidate();
+}
+
 }

+ 8 - 0
Userland/Services/WindowServer/Window.h

@@ -42,6 +42,7 @@ namespace WindowServer {
 class ClientConnection;
 class Cursor;
 class Menu;
+class MenuBar;
 class MenuItem;
 class MouseEvent;
 
@@ -192,6 +193,7 @@ public:
 
     void invalidate(bool with_frame = true, bool re_render_frame = false);
     void invalidate(const Gfx::IntRect&, bool with_frame = false);
+    void invalidate_menubar();
     bool invalidate_no_notify(const Gfx::IntRect& rect, bool with_frame = false);
 
     void refresh_client_size();
@@ -326,6 +328,10 @@ public:
     Gfx::DisjointRectSet& transparency_rects() { return m_transparency_rects; }
     Gfx::DisjointRectSet& transparency_wallpaper_rects() { return m_transparency_wallpaper_rects; }
 
+    MenuBar* menubar() { return m_menubar; }
+    const MenuBar* menubar() const { return m_menubar; }
+    void set_menubar(MenuBar*);
+
 private:
     void handle_mouse_event(const MouseEvent&);
     void update_menu_item_text(PopupMenuItem item);
@@ -341,6 +347,8 @@ private:
     Vector<WeakPtr<Window>> m_child_windows;
     Vector<WeakPtr<Window>> m_accessory_windows;
 
+    RefPtr<MenuBar> m_menubar;
+
     String m_title;
     Gfx::IntRect m_rect;
     Gfx::IntRect m_saved_nonfullscreen_rect;

+ 108 - 2
Userland/Services/WindowServer/WindowFrame.cpp

@@ -79,7 +79,8 @@ static Gfx::IntRect frame_rect_for_window(Window& window, const Gfx::IntRect& re
 {
     if (window.is_frameless())
         return rect;
-    return Gfx::WindowTheme::current().frame_rect_for_window(to_theme_window_type(window.type()), rect, WindowManager::the().palette());
+    int menu_row_count = window.menubar() ? 1 : 0;
+    return Gfx::WindowTheme::current().frame_rect_for_window(to_theme_window_type(window.type()), rect, WindowManager::the().palette(), menu_row_count);
 }
 
 WindowFrame::WindowFrame(Window& window)
@@ -238,6 +239,13 @@ void WindowFrame::did_set_maximized(Badge<Window>, bool maximized)
     m_maximize_button->set_icon(maximized ? *s_restore_icon : *s_maximize_icon);
 }
 
+Gfx::IntRect WindowFrame::menubar_rect() const
+{
+    if (!m_window.menubar())
+        return {};
+    return Gfx::WindowTheme::current().menu_bar_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette(), menu_row_count());
+}
+
 Gfx::IntRect WindowFrame::title_bar_rect() const
 {
     return Gfx::WindowTheme::current().title_bar_rect(to_theme_window_type(m_window.type()), m_window.rect(), WindowManager::the().palette());
@@ -289,11 +297,46 @@ void WindowFrame::paint_tool_window_frame(Gfx::Painter& painter)
     Gfx::WindowTheme::current().paint_tool_window_frame(painter, window_state_for_theme(), m_window.rect(), compute_title_text(), palette, leftmost_button_rect);
 }
 
+void WindowFrame::paint_menubar(Gfx::Painter& painter)
+{
+    auto& wm = WindowManager::the();
+    auto& font = wm.font();
+    auto palette = wm.palette();
+    auto menubar_rect = this->menubar_rect();
+
+    painter.fill_rect(menubar_rect, palette.window());
+
+    Gfx::PainterStateSaver saver(painter);
+    painter.translate(menubar_rect.location());
+
+    m_window.menubar()->for_each_menu([&](Menu& menu) {
+        auto text_rect = menu.text_rect_in_window_menubar();
+        Color text_color = palette.window_text();
+        if (MenuManager::the().is_open(menu)) {
+            painter.fill_rect(menu.rect_in_window_menubar(), palette.menu_selection());
+            text_color = palette.menu_selection_text();
+            text_rect.move_by(1, 1);
+        }
+        if (&menu == MenuManager::the().hovered_menu() || MenuManager::the().is_open(menu))
+            Gfx::StylePainter::paint_button(painter, menu.rect_in_window_menubar(), palette, Gfx::ButtonStyle::CoolBar, MenuManager::the().is_open(menu), true);
+        painter.draw_text(
+            text_rect,
+            menu.name(),
+            font,
+            Gfx::TextAlignment::CenterLeft,
+            text_color);
+        return IterationDecision::Continue;
+    });
+}
+
 void WindowFrame::paint_normal_frame(Gfx::Painter& painter)
 {
     auto palette = WindowManager::the().palette();
     auto leftmost_button_rect = m_buttons.is_empty() ? Gfx::IntRect() : m_buttons.last().relative_rect();
-    Gfx::WindowTheme::current().paint_normal_frame(painter, window_state_for_theme(), m_window.rect(), compute_title_text(), m_window.icon(), palette, leftmost_button_rect);
+    Gfx::WindowTheme::current().paint_normal_frame(painter, window_state_for_theme(), m_window.rect(), compute_title_text(), m_window.icon(), palette, leftmost_button_rect, menu_row_count());
+
+    if (m_window.menubar())
+        paint_menubar(painter);
 }
 
 void WindowFrame::paint(Gfx::Painter& painter, const Gfx::IntRect& rect)
@@ -509,6 +552,13 @@ void WindowFrame::invalidate_title_bar()
     invalidate(title_bar_rect());
 }
 
+void WindowFrame::invalidate()
+{
+    auto frame_rect = render_rect();
+    invalidate(Gfx::IntRect { frame_rect.location() - m_window.position(), frame_rect.size() });
+    m_window.invalidate(true, true);
+}
+
 void WindowFrame::invalidate(Gfx::IntRect relative_rect)
 {
     auto frame_rect = rect();
@@ -652,6 +702,13 @@ void WindowFrame::on_mouse_event(const MouseEvent& event)
         return;
     }
 
+    auto menubar_rect = this->menubar_rect();
+    if (menubar_rect.contains(event.position())) {
+        wm.clear_resize_candidate();
+        handle_menubar_mouse_event(event);
+        return;
+    }
+
     if (m_window.is_resizable() && event.type() == Event::MouseMove && event.buttons() == 0) {
         constexpr ResizeDirection direction_for_hot_area[3][3] = {
             { ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
@@ -673,6 +730,50 @@ void WindowFrame::on_mouse_event(const MouseEvent& event)
         wm.start_window_resize(m_window, event.translated(rect().location()));
 }
 
+void WindowFrame::handle_menubar_mouse_event(const MouseEvent& event)
+{
+    Menu* hovered_menu = nullptr;
+    auto menubar_rect = this->menubar_rect();
+    auto adjusted_position = event.position().translated(-menubar_rect.location());
+    m_window.menubar()->for_each_menu([&](Menu& menu) {
+        if (menu.rect_in_window_menubar().contains(adjusted_position)) {
+            hovered_menu = &menu;
+            handle_menu_mouse_event(menu, event);
+            return IterationDecision::Break;
+        }
+        return IterationDecision::Continue;
+    });
+    if (!hovered_menu && event.type() == Event::Type::MouseDown)
+        MenuManager::the().close_everyone();
+    if (hovered_menu != MenuManager::the().hovered_menu()) {
+        MenuManager::the().set_hovered_menu(hovered_menu);
+        invalidate(menubar_rect);
+    }
+}
+
+void WindowFrame::handle_menu_mouse_event(Menu& menu, const MouseEvent& event)
+{
+    auto menubar_rect = this->menubar_rect();
+    bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove && &m_window == WindowManager::the().window_with_active_menu();
+    bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Left;
+    bool should_open_menu = &menu != MenuManager::the().current_menu() && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
+    bool should_close_menu = &menu == MenuManager::the().current_menu() && is_mousedown_with_left_button;
+
+    if (should_open_menu) {
+        MenuManager::the().close_everyone();
+        menu.ensure_menu_window().move_to(menu.rect_in_window_menubar().bottom_left().translated(rect().location()).translated(menubar_rect.location()));
+        MenuManager::the().open_menu(menu, false);
+        WindowManager::the().set_window_with_active_menu(&m_window);
+        invalidate(menubar_rect);
+        return;
+    }
+
+    if (should_close_menu) {
+        invalidate(menubar_rect);
+        MenuManager::the().close_everyone();
+    }
+}
+
 void WindowFrame::start_flash_animation()
 {
     if (!m_flash_timer) {
@@ -774,4 +875,9 @@ void WindowFrame::paint_simple_rect_shadow(Gfx::Painter& painter, const Gfx::Int
     paint_vertical(containing_rect.right() - base_size + 1, 1, 0, horizontal_shift);
 }
 
+int WindowFrame::menu_row_count() const
+{
+    return m_window.menubar() ? 1 : 0;
+}
+
 }

+ 10 - 1
Userland/Services/WindowServer/WindowFrame.h

@@ -36,6 +36,7 @@
 namespace WindowServer {
 
 class Button;
+class Menu;
 class MouseEvent;
 class Window;
 
@@ -43,7 +44,7 @@ class WindowFrame {
 public:
     static void reload_config();
 
-    WindowFrame(Window&);
+    explicit WindowFrame(Window&);
     ~WindowFrame();
 
     Gfx::IntRect rect() const;
@@ -55,11 +56,15 @@ public:
     void notify_window_rect_changed(const Gfx::IntRect& old_rect, const Gfx::IntRect& new_rect);
     void invalidate_title_bar();
     void invalidate(Gfx::IntRect relative_rect);
+    void invalidate();
 
     Gfx::IntRect title_bar_rect() const;
     Gfx::IntRect title_bar_icon_rect() const;
     Gfx::IntRect title_bar_text_rect() const;
 
+    Gfx::IntRect menubar_rect() const;
+    int menu_row_count() const;
+
     void did_set_maximized(Badge<Window>, bool);
 
     void layout_buttons();
@@ -97,11 +102,15 @@ private:
     void paint_notification_frame(Gfx::Painter&);
     void paint_normal_frame(Gfx::Painter&);
     void paint_tool_window_frame(Gfx::Painter&);
+    void paint_menubar(Gfx::Painter&);
     Gfx::Bitmap* window_shadow() const;
     bool frame_has_alpha() const;
     Gfx::IntRect inflated_for_shadow(const Gfx::IntRect&) const;
     Gfx::Bitmap* inflate_for_shadow(Gfx::IntRect&, Gfx::IntPoint&) const;
 
+    void handle_menubar_mouse_event(const MouseEvent&);
+    void handle_menu_mouse_event(Menu&, const MouseEvent&);
+
     Gfx::WindowTheme::WindowState window_state_for_theme() const;
     String compute_title_text() const;
 

+ 35 - 6
Userland/Services/WindowServer/WindowManager.cpp

@@ -206,6 +206,8 @@ void WindowManager::add_window(Window& window)
         });
     }
 
+    window.invalidate(true, true);
+
     tell_wm_listeners_window_state_changed(window);
 }
 
@@ -914,8 +916,21 @@ void WindowManager::process_mouse_event(MouseEvent& event, Window*& hovered_wind
     if (m_hovered_button && event.type() == Event::MouseMove)
         m_hovered_button->on_mouse_event(event.translated(-m_hovered_button->screen_rect().location()));
 
-    // FIXME: Now that the menubar has a dedicated window, is this special-casing really necessary?
-    if (MenuManager::the().has_open_menu() || menubar_rect().contains(event.position())) {
+    bool hitting_menu_in_window_with_active_menu = [&] {
+        if (!m_window_with_active_menu)
+            return false;
+        auto& frame = m_window_with_active_menu->frame();
+        return frame.menubar_rect().contains(event.position().translated(-frame.rect().location()));
+    }();
+
+    // FIXME: This is quite hackish, we clear the hovered menu before potentially setting the same menu
+    //        as hovered again. This makes sure that the hovered state doesn't linger after moving the
+    //        cursor away from a hovered menu.
+    MenuManager::the().set_hovered_menu(nullptr);
+
+    if (MenuManager::the().has_open_menu()
+        || hitting_menu_in_window_with_active_menu
+        || menubar_rect().contains(event.position())) {
         for_each_visible_window_of_type_from_front_to_back(WindowType::MenuApplet, [&](auto& window) {
             if (!window.rect_in_menubar().contains(event.position()))
                 return IterationDecision::Continue;
@@ -923,8 +938,11 @@ void WindowManager::process_mouse_event(MouseEvent& event, Window*& hovered_wind
             return IterationDecision::Break;
         });
         clear_resize_candidate();
-        MenuManager::the().dispatch_event(event);
-        return;
+
+        if (!hitting_menu_in_window_with_active_menu) {
+            MenuManager::the().dispatch_event(event);
+            return;
+        }
     }
 
     Window* event_window_with_frame = nullptr;
@@ -1456,8 +1474,8 @@ Gfx::IntRect WindowManager::maximized_window_rect(const Window& window) const
     Gfx::IntRect rect = Screen::the().rect();
 
     // Subtract window title bar (leaving the border)
-    rect.set_y(rect.y() + window.frame().title_bar_rect().height());
-    rect.set_height(rect.height() - window.frame().title_bar_rect().height());
+    rect.set_y(rect.y() + window.frame().title_bar_rect().height() + window.frame().menubar_rect().height());
+    rect.set_height(rect.height() - window.frame().title_bar_rect().height() - window.frame().menubar_rect().height());
 
     // Subtract menu bar
     rect.set_y(rect.y() + menubar_rect().height());
@@ -1611,4 +1629,15 @@ void WindowManager::reload_icon_bitmaps_after_scale_change(bool allow_hidpi_icon
         return IterationDecision::Continue;
     });
 }
+
+void WindowManager::set_window_with_active_menu(Window* window)
+{
+    if (m_window_with_active_menu == window)
+        return;
+    if (window)
+        m_window_with_active_menu = window->make_weak_ptr<Window>();
+    else
+        m_window_with_active_menu = nullptr;
+}
+
 }

+ 5 - 0
Userland/Services/WindowServer/WindowManager.h

@@ -113,6 +113,10 @@ public:
     const Window* active_input_window() const { return m_active_input_window.ptr(); }
     const ClientConnection* active_client() const;
 
+    Window* window_with_active_menu() { return m_window_with_active_menu; }
+    const Window* window_with_active_menu() const { return m_window_with_active_menu; }
+    void set_window_with_active_menu(Window*);
+
     const Window* highlight_window() const { return m_highlight_window.ptr(); }
     void set_highlight_window(Window*);
 
@@ -329,6 +333,7 @@ private:
     WeakPtr<Window> m_highlight_window;
     WeakPtr<Window> m_active_input_window;
     WeakPtr<Window> m_active_input_tracking_window;
+    WeakPtr<Window> m_window_with_active_menu;
 
     WeakPtr<Window> m_move_window;
     Gfx::IntPoint m_move_origin;

+ 2 - 0
Userland/Services/WindowServer/WindowServer.ipc

@@ -52,6 +52,8 @@ endpoint WindowServer = 2
 
     DestroyWindow(i32 window_id) => (Vector<i32> destroyed_window_ids)
 
+    SetWindowMenubar(i32 window_id, i32 menubar_id) => ()
+
     SetWindowTitle(i32 window_id, [UTF8] String title) => ()
     GetWindowTitle(i32 window_id) => ([UTF8] String title)