diff --git a/Kernel/Makefile b/Kernel/Makefile index d92e06325a..db6f6e5217 100644 --- a/Kernel/Makefile +++ b/Kernel/Makefile @@ -63,6 +63,9 @@ WINDOWSERVER_OBJS = \ ../WindowServer/WSWindow.o \ ../WindowServer/WSWindowManager.o \ ../WindowServer/WSScreen.o \ + ../WindowServer/WSMenuBar.o \ + ../WindowServer/WSMenu.o \ + ../WindowServer/WSMenuItem.o \ ../WindowServer/main.o AK_OBJS = \ diff --git a/SharedGraphics/Font.h b/SharedGraphics/Font.h index 20b7dcfe7f..804d917000 100644 --- a/SharedGraphics/Font.h +++ b/SharedGraphics/Font.h @@ -58,6 +58,7 @@ public: byte glyph_width() const { return m_glyph_width; } byte glyph_height() const { return m_glyph_height; } + int width(const String& string) const { return string.length() * glyph_width(); } String name() const { return m_name; } void set_name(const String& name) { m_name = name; } diff --git a/SharedGraphics/GraphicsBitmap.cpp b/SharedGraphics/GraphicsBitmap.cpp index 0c9dc70519..5d0183b806 100644 --- a/SharedGraphics/GraphicsBitmap.cpp +++ b/SharedGraphics/GraphicsBitmap.cpp @@ -39,6 +39,25 @@ GraphicsBitmap::GraphicsBitmap(Process& process, const Size& size) m_data = (RGBA32*)m_server_region->laddr().as_ptr(); } + +RetainPtr GraphicsBitmap::create_kernel_only(const Size& size) +{ + return adopt(*new GraphicsBitmap(size)); +} + +GraphicsBitmap::GraphicsBitmap(const Size& size) + : m_size(size) + , m_pitch(size.width() * sizeof(RGBA32)) +{ + InterruptDisabler disabler; + size_t size_in_bytes = size.width() * size.height() * sizeof(RGBA32); + auto vmo = VMObject::create_anonymous(size_in_bytes); + auto& server = WSMessageLoop::the().server_process(); + m_server_region = server.allocate_region_with_vmo(LinearAddress(), size_in_bytes, move(vmo), 0, "GraphicsBitmap (server)", true, false); + m_server_region->set_shared(true); + m_server_region->set_is_bitmap(true); + m_data = (RGBA32*)m_server_region->laddr().as_ptr(); +} #endif RetainPtr GraphicsBitmap::create_wrapper(const Size& size, RGBA32* data) diff --git a/SharedGraphics/GraphicsBitmap.h b/SharedGraphics/GraphicsBitmap.h index 861e0d4170..608575796d 100644 --- a/SharedGraphics/GraphicsBitmap.h +++ b/SharedGraphics/GraphicsBitmap.h @@ -15,6 +15,7 @@ class GraphicsBitmap : public Retainable { public: #ifdef KERNEL static RetainPtr create(Process&, const Size&); + static RetainPtr create_kernel_only(const Size&); #endif static RetainPtr create_wrapper(const Size&, RGBA32*); static RetainPtr load_from_file(const String& path, const Size&); @@ -37,6 +38,7 @@ public: private: #ifdef KERNEL GraphicsBitmap(Process&, const Size&); + GraphicsBitmap(const Size&); #endif GraphicsBitmap(const Size&, RGBA32*); diff --git a/WindowServer/WSMenu.cpp b/WindowServer/WSMenu.cpp new file mode 100644 index 0000000000..bc77860611 --- /dev/null +++ b/WindowServer/WSMenu.cpp @@ -0,0 +1,111 @@ +#include "WSMenu.h" +#include "WSMenuItem.h" +#include "WSWindow.h" +#include "WSMessage.h" +#include "WSMessageLoop.h" +#include +#include + +WSMenu::WSMenu(const String& name) + : m_name(name) +{ +} + +WSMenu::~WSMenu() +{ +} + +void WSMenu::set_menu_window(OwnPtr&& menu_window) +{ + m_menu_window = move(menu_window); +} + +const Font& WSMenu::font() const +{ + return Font::default_font(); +} + +int WSMenu::width() const +{ + int longest = 0; + for (auto& item : m_items) { + if (item->type() == WSMenuItem::Text) + longest = max(longest, font().width(item->text())); + } + + return max(longest, 80) + padding() * 2; +} + +int WSMenu::height() const +{ + if (m_items.is_empty()) + return 0; + return (m_items.last()->rect().bottom() - 1) + padding(); +} + +WSWindow& WSMenu::ensure_menu_window() +{ + if (!m_menu_window) { + Point next_item_location(padding() / 2, padding() / 2); + for (auto& item : m_items) { + int height = 0; + if (item->type() == WSMenuItem::Text) + height = item_height(); + else if (item->type() == WSMenuItem::Separator) + height = 7; + item->set_rect({ next_item_location, { width() - padding(), height } }); + next_item_location.move_by(0, height); + } + + auto window = make(*this); + window->set_rect(0, 0, width(), height()); + dbgprintf("Created menu window for menu '%s' (%u items) with rect %s\n", name().characters(), m_items.size(), window->rect().to_string().characters()); + m_menu_window = move(window); + draw(); + } + return *m_menu_window; +} + +void WSMenu::draw() +{ + ASSERT(m_menu_window); + ASSERT(m_menu_window->backing()); + Painter painter(*m_menu_window->backing()); + + Rect rect { { }, m_menu_window->size() }; + + painter.draw_rect(rect, Color::White); + painter.fill_rect(rect.shrunken(2, 2), Color::LightGray); + + for (auto& item : m_items) { + if (item->type() == WSMenuItem::Text) { + Color text_color = Color::Black; + if (item.ptr() == m_hovered_item) { + painter.fill_rect(item->rect(), Color(0, 0, 128)); + text_color = Color::White; + } + painter.draw_text(item->rect(), item->text(), Painter::TextAlignment::CenterLeft, text_color); + } else if (item->type() == WSMenuItem::Separator) { + Point p1(padding(), item->rect().center().y()); + Point p2(width() - padding(), item->rect().center().y()); + painter.draw_line(p1, p2, Color::MidGray); + } + } +} + +void WSMenu::on_window_message(WSMessage& message) +{ + dbgprintf("WSMenu::on_window_message: %u\n", message.type()); + if (message.type() == WSMessage::MouseMove) { + auto& mouse_event = static_cast(message); + for (auto& item : m_items) { + if (item->rect().contains(mouse_event.position())) { + if (m_hovered_item == item.ptr()) + return; + m_hovered_item = item.ptr(); + draw(); + menu_window()->invalidate(); + } + } + } +} diff --git a/WindowServer/WSMenu.h b/WindowServer/WSMenu.h new file mode 100644 index 0000000000..bb4376a536 --- /dev/null +++ b/WindowServer/WSMenu.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include "WSMenuItem.h" + +class WSMenuBar; +class WSMessage; +class WSWindow; +class Font; + +class WSMenu { +public: + WSMenu(const String& name); + ~WSMenu(); + + WSMenuBar* menu_bar() { return m_menubar; } + const WSMenuBar* menu_bar() const { return m_menubar; } + + int item_count() const { return m_items.size(); } + WSMenuItem* item(int i) { return m_items[i].ptr(); } + const WSMenuItem* item(int i) const { return m_items[i].ptr(); } + + void add_item(OwnPtr&& item) { m_items.append(move(item)); } + + String name() const { return m_name; } + + template + void for_each_item(Callback callback) const + { + for (auto& item : m_items) + callback(*item); + } + + Rect text_rect_in_menubar() const { return m_text_rect_in_menubar; } + void set_text_rect_in_menubar(const Rect& rect) { m_text_rect_in_menubar = rect; } + + Rect rect_in_menubar() const { return m_rect_in_menubar; } + void set_rect_in_menubar(const Rect& rect) { m_rect_in_menubar = rect; } + + WSWindow* menu_window() { return m_menu_window.ptr(); } + void set_menu_window(OwnPtr&&); + + WSWindow& ensure_menu_window(); + + int width() const; + int height() const; + + int item_height() const { return 16; } + int padding() const { return 4; } + + void on_window_message(WSMessage&); + void draw(); + const Font& font() const; + +private: + String m_name; + Rect m_rect_in_menubar; + Rect m_text_rect_in_menubar; + WSMenuBar* m_menubar { nullptr }; + WSMenuItem* m_hovered_item { nullptr }; + Vector> m_items; + OwnPtr m_menu_window; +}; + diff --git a/WindowServer/WSMenuBar.cpp b/WindowServer/WSMenuBar.cpp new file mode 100644 index 0000000000..80e6db9546 --- /dev/null +++ b/WindowServer/WSMenuBar.cpp @@ -0,0 +1,12 @@ +#include "WSMenuBar.h" +#include "WSMenu.h" +#include "WSMenuItem.h" + +WSMenuBar::WSMenuBar() +{ +} + +WSMenuBar::~WSMenuBar() +{ +} + diff --git a/WindowServer/WSMenuBar.h b/WindowServer/WSMenuBar.h new file mode 100644 index 0000000000..7cfc7baed4 --- /dev/null +++ b/WindowServer/WSMenuBar.h @@ -0,0 +1,24 @@ +#pragma once + +#include "WSMenu.h" +#include + +class WSMenuBar { +public: + WSMenuBar(); + ~WSMenuBar(); + + void add_menu(OwnPtr&& menu) { m_menus.append(move(menu)); } + + template + void for_each_menu(Callback callback) + { + for (auto& menu : m_menus) { + if (!callback(*menu)) + return; + } + } + +private: + Vector> m_menus; +}; diff --git a/WindowServer/WSMenuItem.cpp b/WindowServer/WSMenuItem.cpp new file mode 100644 index 0000000000..2a94937210 --- /dev/null +++ b/WindowServer/WSMenuItem.cpp @@ -0,0 +1,16 @@ +#include "WSMenuItem.h" + +WSMenuItem::WSMenuItem(const String& text) + : m_type(Text) + , m_text(text) +{ +} + +WSMenuItem::WSMenuItem(Type type) + : m_type(type) +{ +} + +WSMenuItem::~WSMenuItem() +{ +} diff --git a/WindowServer/WSMenuItem.h b/WindowServer/WSMenuItem.h new file mode 100644 index 0000000000..36ffd51e5f --- /dev/null +++ b/WindowServer/WSMenuItem.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +class WSMenuItem { +public: + enum Type { + None, + Text, + Separator, + }; + + explicit WSMenuItem(const String& text); + explicit WSMenuItem(Type); + ~WSMenuItem(); + + Type type() const { return m_type; } + bool enabled() const { return m_enabled; } + + String text() const { return m_text; } + + void set_rect(const Rect& rect) { m_rect = rect; } + Rect rect() const { return m_rect; } + +private: + Type m_type { None }; + bool m_enabled { true }; + String m_text; + Rect m_rect; +}; + diff --git a/WindowServer/WSWindow.cpp b/WindowServer/WSWindow.cpp index d651c916f7..517bbb27fa 100644 --- a/WindowServer/WSWindow.cpp +++ b/WindowServer/WSWindow.cpp @@ -4,8 +4,15 @@ #include "WSMessageLoop.h" #include "Process.h" +WSWindow::WSWindow(WSMenu& menu) + : m_lock("WSWindow (menu)") + , m_menu(&menu) +{ + WSWindowManager::the().add_window(*this); +} + WSWindow::WSWindow(Process& process, int window_id) - : m_lock("WSWindow") + : m_lock("WSWindow (normal)") , m_process(&process) , m_window_id(window_id) , m_pid(process.pid()) @@ -35,13 +42,18 @@ void WSWindow::set_rect(const Rect& rect) Rect old_rect; { WSWindowLocker locker(*this); - if (!m_process) + if (!m_process && !m_menu) return; if (m_rect == rect) return; old_rect = m_rect; m_rect = rect; - m_backing = GraphicsBitmap::create(*m_process, m_rect.size()); + if (!m_backing || old_rect.size() != rect.size()) { + if (m_process) + m_backing = GraphicsBitmap::create(*m_process, m_rect.size()); + if (m_menu) + m_backing = GraphicsBitmap::create_kernel_only(m_rect.size()); + } } WSWindowManager::the().notify_rect_changed(*this, old_rect, rect); } @@ -59,6 +71,14 @@ static GUI_MouseButton to_api(MouseButton button) void WSWindow::on_message(WSMessage& message) { + if (m_menu) { + m_menu->on_window_message(message); + return; + } + + if (!m_process) + return; + GUI_Event gui_event; gui_event.window_id = window_id(); @@ -149,3 +169,16 @@ void WSWindow::set_global_cursor_tracking_enabled(bool enabled) dbgprintf("WSWindow{%p} global_cursor_tracking <- %u\n", enabled); m_global_cursor_tracking_enabled = enabled; } + +void WSWindow::set_visible(bool b) +{ + if (m_visible == b) + return; + m_visible = b; + invalidate(); +} + +void WSWindow::invalidate() +{ + WSWindowManager::the().invalidate(*this); +} diff --git a/WindowServer/WSWindow.h b/WindowServer/WSWindow.h index d09cf18d9c..9d6f9f1016 100644 --- a/WindowServer/WSWindow.h +++ b/WindowServer/WSWindow.h @@ -10,13 +10,16 @@ #include "WSMessageReceiver.h" class Process; +class WSMenu; class WSWindow final : public WSMessageReceiver, public InlineLinkedListNode { friend class WSWindowLocker; public: WSWindow(Process&, int window_id); + explicit WSWindow(WSMenu&); virtual ~WSWindow() override; + bool is_menu() const { return m_menu; } int window_id() const { return m_window_id; } String title() const { return m_title; } @@ -27,14 +30,25 @@ public: int width() const { return m_rect.width(); } int height() const { return m_rect.height(); } - const Rect& rect() const { return m_rect; } + bool is_visible() const { return m_visible; } + void set_visible(bool); + + Rect rect() const { return m_rect; } void set_rect(const Rect&); + void set_rect(int x, int y, int width, int height) { set_rect({ x, y, width, height }); } void set_rect_without_repaint(const Rect& rect) { m_rect = rect; } + void move_to(const Point& position) { set_rect({ position, size() }); } + void move_to(int x, int y) { move_to({ x, y }); } + Point position() const { return m_rect.location(); } void set_position(const Point& position) { set_rect({ position.x(), position.y(), width(), height() }); } void set_position_without_repaint(const Point& position) { set_rect_without_repaint({ position.x(), position.y(), width(), height() }); } + Size size() const { return m_rect.size(); } + + void invalidate(); + virtual void on_message(WSMessage&) override; bool is_being_dragged() const { return m_is_being_dragged; } @@ -60,6 +74,9 @@ private: Rect m_rect; bool m_is_being_dragged { false }; bool m_global_cursor_tracking_enabled { false }; + bool m_visible { true }; + + WSMenu* m_menu { nullptr }; RetainPtr m_backing; Process* m_process { nullptr }; diff --git a/WindowServer/WSWindowManager.cpp b/WindowServer/WSWindowManager.cpp index 735daad45f..16e44b8505 100644 --- a/WindowServer/WSWindowManager.cpp +++ b/WindowServer/WSWindowManager.cpp @@ -10,6 +10,9 @@ #include #include #include +#include "WSMenu.h" +#include "WSMenuBar.h" +#include "WSMenuItem.h" //#define DEBUG_COUNTERS //#define DEBUG_WID_IN_TITLE_BAR @@ -176,6 +179,32 @@ WSWindowManager::WSWindowManager() invalidate(m_screen_rect); }); + + { + auto menubar = make(); + + { + auto menu = make("Serenity"); + menu->add_item(make("Launch Terminal")); + menu->add_item(make(WSMenuItem::Separator)); + menu->add_item(make("Hello again")); + menu->add_item(make("To all my friends")); + menu->add_item(make("Together we can play some rock&roll")); + menu->add_item(make(WSMenuItem::Separator)); + menu->add_item(make("About...")); + menubar->add_menu(move(menu)); + } + { + auto menu = make("Application"); + menu->add_item(make("Bar!")); + menu->add_item(make("Foo!")); + menubar->add_menu(move(menu)); + } + + set_current_menubar(menubar.ptr()); + m_menubars.set(1, move(menubar)); + } + invalidate(); compose(); } @@ -184,6 +213,24 @@ WSWindowManager::~WSWindowManager() { } +void WSWindowManager::set_current_menubar(WSMenuBar* menubar) +{ + if (m_current_menubar == menubar) + return; + m_current_menubar = menubar; + if (!m_current_menubar) + return; + int menu_margin = 16; + Point next_menu_location { menu_margin / 2, 3 }; + m_current_menubar->for_each_menu([&] (WSMenu& menu) { + int text_width = font().width(menu.name()); + menu.set_rect_in_menubar({ next_menu_location.x() - menu_margin / 2, 0, text_width + menu_margin, menubar_rect().height() - 1 }); + menu.set_text_rect_in_menubar({ next_menu_location, { text_width, font().glyph_height() } }); + next_menu_location.move_by(menu.rect_in_menubar().width(), 0); + return true; + }); +} + static const char* s_close_button_bitmap_data = { "## ##" "### ###" @@ -205,6 +252,11 @@ void WSWindowManager::paint_window_frame(WSWindow& window) LOCKER(m_lock); //printf("[WM] paint_window_frame {%p}, rect: %d,%d %dx%d\n", &window, window.rect().x(), window.rect().y(), window.rect().width(), window.rect().height()); + if (window.is_menu()) { + m_back_painter->draw_rect(window.rect().inflated(2, 2), Color::LightGray); + return; + } + auto titlebar_rect = title_bar_rect(window.rect()); auto titlebar_inner_rect = title_bar_text_rect(window.rect()); auto outer_rect = outer_window_rect(window.rect()); @@ -318,6 +370,43 @@ void WSWindowManager::notify_rect_changed(WSWindow& window, const Rect& old_rect invalidate(outer_window_rect(new_rect)); } +void WSWindowManager::handle_menu_mouse_event(WSMenu& menu, WSMouseEvent& event) +{ + if (event.type() == WSMouseEvent::MouseDown && event.button() == MouseButton::Left) { + dbgprintf("[WM] MouseDown on menu '%s'\n", menu.name().characters()); + if (m_current_menu == &menu) + return; + close_current_menu(); + auto& menu_window = menu.ensure_menu_window(); + menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() }); + menu_window.set_visible(true); + m_current_menu = &menu; + return; + } + if (event.type() == WSMouseEvent::MouseUp && event.button() == MouseButton::Left) { + close_current_menu(); + return; + } +} + +void WSWindowManager::close_current_menu() +{ + if (m_current_menu && m_current_menu->menu_window()) + m_current_menu->menu_window()->set_visible(false); + m_current_menu = nullptr; +} + +void WSWindowManager::handle_menubar_mouse_event(WSMenuBar& menu, WSMouseEvent& event) +{ + m_current_menubar->for_each_menu([&] (WSMenu& menu) { + if (menu.rect_in_menubar().contains(event.position())) { + handle_menu_mouse_event(menu, event); + return false; + } + return true; + }); +} + void WSWindowManager::handle_titlebar_mouse_event(WSWindow& window, WSMouseEvent& event) { if (event.type() == WSMessage::MouseDown && event.button() == MouseButton::Left) { @@ -376,12 +465,23 @@ void WSWindowManager::process_mouse_event(WSMouseEvent& event) for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { if (!window->global_cursor_tracking()) continue; + ASSERT(window->is_visible()); // Maybe this should be supported? Idk. Let's catch it and think about it later. Point position { event.x() - window->rect().x(), event.y() - window->rect().y() }; auto local_event = make(event.type(), position, event.buttons(), event.button()); window->on_message(*local_event); } + if (m_current_menubar && menubar_rect().contains(event.position())) { + handle_menubar_mouse_event(*m_current_menubar, event); + return; + } + + if (m_current_menu && event.type() == WSMouseEvent::MouseUp && event.button() == MouseButton::Left) + close_current_menu(); + for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) { + if (!window->is_visible()) + continue; if (title_bar_rect(window->rect()).contains(event.position())) { if (event.type() == WSMessage::MouseDown) { move_to_front(*window); @@ -423,6 +523,8 @@ void WSWindowManager::compose() auto any_window_contains_rect = [this] (const Rect& r) { for (auto* window = m_windows_in_order.head(); window; window = window->next()) { + if (!window->is_visible()) + continue; if (outer_window_rect(window->rect()).contains(r)) return true; } @@ -449,6 +551,8 @@ void WSWindowManager::compose() m_back_painter->blit(dirty_rect.location(), *m_wallpaper, dirty_rect); } for (auto* window = m_windows_in_order.head(); window; window = window->next()) { + if (!window->is_visible()) + continue; WSWindowLocker locker(*window); RetainPtr backing = window->backing(); if (!backing) @@ -470,8 +574,9 @@ void WSWindowManager::compose() } m_back_painter->clear_clip_rect(); } - draw_cursor(); + draw_menubar(); + draw_cursor(); if (m_flash_flush.lock_and_copy()) { for (auto& rect : dirty_rects) @@ -491,6 +596,28 @@ void WSWindowManager::invalidate_cursor() invalidate(cursor_rect); } +Rect WSWindowManager::menubar_rect() const +{ + return { 0, 0, m_screen_rect.width(), 16 }; +} + +void WSWindowManager::draw_menubar() +{ + if (!m_current_menubar) + return; + m_back_painter->fill_rect(menubar_rect(), Color::LightGray); + m_back_painter->draw_line({ 0, menubar_rect().bottom() }, { menubar_rect().right(), menubar_rect().bottom() }, Color::White); + m_current_menubar->for_each_menu([&] (WSMenu& menu) { + Color text_color = Color::Black; + if (&menu == m_current_menu) { + m_back_painter->fill_rect(menu.rect_in_menubar(), Color(0, 0, 128)); + text_color = Color::White; + } + m_back_painter->draw_text(menu.text_rect_in_menubar(), menu.name(), Painter::TextAlignment::CenterLeft, text_color); + return true; + }); +} + void WSWindowManager::draw_cursor() { ASSERT_INTERRUPTS_ENABLED(); @@ -619,3 +746,4 @@ void WSWindowManager::flush(const Rect& a_rect) back_ptr = (RGBA32*)((byte*)back_ptr + pitch); } } + diff --git a/WindowServer/WSWindowManager.h b/WindowServer/WSWindowManager.h index 18e32fcfff..c181d8698f 100644 --- a/WindowServer/WSWindowManager.h +++ b/WindowServer/WSWindowManager.h @@ -7,10 +7,12 @@ #include #include #include -#include +#include #include "WSMessageReceiver.h" +#include "WSMenuBar.h" class WSScreen; +class WSMenuBar; class WSMouseEvent; class WSClientWantsToPaintMessage; class WSWindow; @@ -32,6 +34,13 @@ public: void invalidate_cursor(); void draw_cursor(); + void draw_menubar(); + + Rect menubar_rect() const; + WSMenuBar* current_menubar() { return m_current_menubar; } + void set_current_menubar(WSMenuBar*); + WSMenu* current_menu() { return m_current_menu; } + void set_current_menu(WSMenu*); void invalidate(const WSWindow&); void invalidate(const WSWindow&, const Rect&); @@ -47,10 +56,14 @@ private: virtual ~WSWindowManager() override; void process_mouse_event(WSMouseEvent&); + void handle_menu_mouse_event(WSMenu&, WSMouseEvent&); + void handle_menubar_mouse_event(WSMenuBar&, WSMouseEvent&); void handle_titlebar_mouse_event(WSWindow&, WSMouseEvent&); void handle_close_button_mouse_event(WSWindow&, WSMouseEvent&); void set_active_window(WSWindow*); + + void close_current_menu(); virtual void on_message(WSMessage&) override; @@ -112,4 +125,8 @@ private: Lockable m_flash_flush; bool m_buffers_are_flipped { false }; + + WSMenuBar* m_current_menubar { nullptr }; + WSMenu* m_current_menu { nullptr }; + HashMap> m_menubars; };