Skip to content

Commit fb7a885

Browse files
committed
WindowServer: Allow scrolling of menus that don't fit on screen
Menus now have a scroll offset (index based, not pixel based) which is controlled either with the mouse wheel or with the up/down arrow keys. This finally allows us to browse all of the fonts that @xTibor has made avilable through his serenity-fontdev project: https://github.com/xTibor/serenity-fontdev I'm not completely sure about the up/down arrows. They feel like maybe they occupy a bit too much vertical space. Also FIXME: this mechanism probably won't look completely right for menus that have separators in them. Fixes #1043.
1 parent 5ce1cc8 commit fb7a885

File tree

4 files changed

+115
-46
lines changed

4 files changed

+115
-46
lines changed

Servers/WindowServer/WSMenu.cpp

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ static const int s_item_icon_width = 16;
9292
static const int s_checkbox_or_icon_padding = 6;
9393
static const int s_stripe_width = 23;
9494

95-
int WSMenu::width() const
95+
int WSMenu::content_width() const
9696
{
9797
int widest_text = 0;
9898
int widest_shortcut = 0;
@@ -114,13 +114,6 @@ int WSMenu::width() const
114114
return max(widest_item, rect_in_menubar().width()) + horizontal_padding() + frame_thickness() * 2;
115115
}
116116

117-
int WSMenu::height() const
118-
{
119-
if (m_items.is_empty())
120-
return 0;
121-
return (m_items.last().rect().bottom() + 1) + frame_thickness();
122-
}
123-
124117
void WSMenu::redraw()
125118
{
126119
if (!menu_window())
@@ -131,7 +124,7 @@ void WSMenu::redraw()
131124

132125
WSWindow& WSMenu::ensure_menu_window()
133126
{
134-
int width = this->width();
127+
int width = this->content_width();
135128
if (!m_menu_window) {
136129
Point next_item_location(frame_thickness(), frame_thickness());
137130
for (auto& item : m_items) {
@@ -144,14 +137,32 @@ WSWindow& WSMenu::ensure_menu_window()
144137
next_item_location.move_by(0, height);
145138
}
146139

140+
int window_height_available = WSScreen::the().height() - WSMenuManager::the().menubar_rect().height() - frame_thickness() * 2;
141+
int max_window_height = (window_height_available / item_height()) * item_height() + frame_thickness() * 2;
142+
int content_height = m_items.is_empty() ? 0 : (m_items.last().rect().bottom() + 1) + frame_thickness();
143+
int window_height = min(max_window_height, content_height);
144+
if (window_height < content_height) {
145+
m_scrollable = true;
146+
m_max_scroll_offset = item_count() - window_height / item_height() + 2;
147+
}
148+
147149
auto window = WSWindow::construct(*this, WSWindowType::Menu);
148-
window->set_rect(0, 0, width, height());
150+
window->set_rect(0, 0, width, window_height);
149151
m_menu_window = move(window);
150152
draw();
151153
}
152154
return *m_menu_window;
153155
}
154156

157+
int WSMenu::visible_item_count() const
158+
{
159+
if (!is_scrollable())
160+
return m_items.size();
161+
ASSERT(m_menu_window);
162+
// Make space for up/down arrow indicators
163+
return m_menu_window->height() / item_height() - 2;
164+
}
165+
155166
void WSMenu::draw()
156167
{
157168
auto palette = WSWindowManager::the().palette();
@@ -164,7 +175,7 @@ void WSMenu::draw()
164175
Rect rect { {}, menu_window()->size() };
165176
painter.fill_rect(rect.shrunken(6, 6), palette.menu_base());
166177
StylePainter::paint_window_frame(painter, rect, palette);
167-
int width = this->width();
178+
int width = this->content_width();
168179

169180
if (!s_checked_bitmap)
170181
s_checked_bitmap = &CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref();
@@ -176,11 +187,23 @@ void WSMenu::draw()
176187
has_items_with_icon = has_items_with_icon | !!item.icon();
177188
}
178189

179-
Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, height() - frame_thickness() * 2 };
190+
Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, menu_window()->height() - frame_thickness() * 2 };
180191
painter.fill_rect(stripe_rect, palette.menu_stripe());
181192
painter.draw_line(stripe_rect.top_right(), stripe_rect.bottom_right(), palette.menu_stripe().darkened());
182193

183-
for (auto& item : m_items) {
194+
int visible_item_count = this->visible_item_count();
195+
196+
if (is_scrollable()) {
197+
bool can_go_up = m_scroll_offset > 0;
198+
bool can_go_down = m_scroll_offset < m_max_scroll_offset;
199+
Rect up_indicator_rect { frame_thickness(), frame_thickness(), content_width(), item_height() };
200+
painter.draw_text(up_indicator_rect, "\xc3\xb6", TextAlignment::Center, can_go_up ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
201+
Rect down_indicator_rect { frame_thickness(), menu_window()->height() - item_height() - frame_thickness(), content_width(), item_height() };
202+
painter.draw_text(down_indicator_rect, "\xc3\xb7", TextAlignment::Center, can_go_down ? palette.menu_base_text() : palette.color(ColorRole::DisabledText));
203+
}
204+
205+
for (int i = 0; i < visible_item_count; ++i) {
206+
auto& item = m_items.at(m_scroll_offset + i);
184207
if (item.type() == WSMenuItem::Text) {
185208
Color text_color = palette.menu_base_text();
186209
if (&item == hovered_item() && item.is_enabled()) {
@@ -277,34 +300,40 @@ void WSMenu::decend_into_submenu_at_hovered_item()
277300
m_in_submenu = true;
278301
}
279302

280-
void WSMenu::event(CEvent& event)
303+
void WSMenu::handle_hover_event(const WSMouseEvent& event)
281304
{
282-
if (event.type() == WSEvent::MouseMove) {
283-
ASSERT(menu_window());
284-
auto mouse_event = static_cast<const WSMouseEvent&>(event);
305+
ASSERT(menu_window());
306+
auto mouse_event = static_cast<const WSMouseEvent&>(event);
285307

286-
if (hovered_item() && hovered_item()->is_submenu()) {
308+
if (hovered_item() && hovered_item()->is_submenu()) {
287309

288-
auto item = *hovered_item();
289-
auto submenu_top_left = item.rect().location() + Point { item.rect().width(), 0 };
290-
auto submenu_bottom_left = submenu_top_left + Point { 0, item.submenu()->height() };
310+
auto item = *hovered_item();
311+
auto submenu_top_left = item.rect().location() + Point { item.rect().width(), 0 };
312+
auto submenu_bottom_left = submenu_top_left + Point { 0, item.submenu()->menu_window()->height() };
291313

292-
auto safe_hover_triangle = Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left };
293-
m_last_position_in_hover = mouse_event.position();
314+
auto safe_hover_triangle = Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left };
315+
m_last_position_in_hover = mouse_event.position();
294316

295-
// Don't update the hovered item if mouse is moving towards a submenu
296-
if (safe_hover_triangle.contains(mouse_event.position()))
297-
return;
298-
}
299-
300-
int index = item_index_at(mouse_event.position());
301-
if (m_hovered_item_index == index)
317+
// Don't update the hovered item if mouse is moving towards a submenu
318+
if (safe_hover_triangle.contains(mouse_event.position()))
302319
return;
303-
m_hovered_item_index = index;
320+
}
304321

305-
// FIXME: Tell parent menu (if it exists) that it is currently in a submenu
306-
m_in_submenu = false;
307-
update_for_new_hovered_item();
322+
int index = item_index_at(mouse_event.position());
323+
if (m_hovered_item_index == index)
324+
return;
325+
m_hovered_item_index = index;
326+
327+
// FIXME: Tell parent menu (if it exists) that it is currently in a submenu
328+
m_in_submenu = false;
329+
update_for_new_hovered_item();
330+
return;
331+
}
332+
333+
void WSMenu::event(CEvent& event)
334+
{
335+
if (event.type() == WSEvent::MouseMove) {
336+
handle_hover_event(static_cast<const WSMouseEvent&>(event));
308337
return;
309338
}
310339

@@ -313,6 +342,18 @@ void WSMenu::event(CEvent& event)
313342
return;
314343
}
315344

345+
if (event.type() == WSEvent::MouseWheel && is_scrollable()) {
346+
auto& mouse_event = static_cast<const WSMouseEvent&>(event);
347+
m_scroll_offset += mouse_event.wheel_delta();
348+
if (m_scroll_offset < 0)
349+
m_scroll_offset = 0;
350+
if (m_scroll_offset >= m_max_scroll_offset)
351+
m_scroll_offset = m_max_scroll_offset;
352+
handle_hover_event(mouse_event);
353+
redraw();
354+
return;
355+
}
356+
316357
if (event.type() == WSEvent::KeyDown) {
317358
auto key = static_cast<WSKeyEvent&>(event).key();
318359

@@ -347,25 +388,37 @@ void WSMenu::event(CEvent& event)
347388
if (key == Key_Up) {
348389
ASSERT(m_items.at(0).type() != WSMenuItem::Separator);
349390

391+
if (is_scrollable() && m_hovered_item_index == 0)
392+
return;
393+
350394
do {
351395
m_hovered_item_index--;
352396
if (m_hovered_item_index < 0)
353397
m_hovered_item_index = m_items.size() - 1;
354398
} while (hovered_item()->type() == WSMenuItem::Separator);
355399

400+
if (is_scrollable() && m_hovered_item_index < m_scroll_offset)
401+
--m_scroll_offset;
402+
356403
update_for_new_hovered_item();
357404
return;
358405
}
359406

360407
if (key == Key_Down) {
361408
ASSERT(m_items.at(0).type() != WSMenuItem::Separator);
362409

410+
if (is_scrollable() && m_hovered_item_index == m_items.size() - 1)
411+
return;
412+
363413
do {
364414
m_hovered_item_index++;
365415
if (m_hovered_item_index >= m_items.size())
366416
m_hovered_item_index = 0;
367417
} while (hovered_item()->type() == WSMenuItem::Separator);
368418

419+
if (is_scrollable() && m_hovered_item_index >= (m_scroll_offset + visible_item_count()))
420+
++m_scroll_offset;
421+
369422
update_for_new_hovered_item();
370423
return;
371424
}
@@ -452,16 +505,16 @@ void WSMenu::popup(const Point& position, bool is_submenu)
452505

453506
const int margin = 30;
454507
Point adjusted_pos = position;
455-
if (window.height() >= WSScreen::the().height()) {
456-
adjusted_pos.set_y(0);
457-
} else {
458-
if (adjusted_pos.x() + window.width() >= WSScreen::the().width() - margin) {
459-
adjusted_pos = adjusted_pos.translated(-window.width(), 0);
460-
}
461-
if (adjusted_pos.y() + window.height() >= WSScreen::the().height() - margin) {
462-
adjusted_pos = adjusted_pos.translated(0, -window.height());
463-
}
508+
509+
if (adjusted_pos.x() + window.width() >= WSScreen::the().width() - margin) {
510+
adjusted_pos = adjusted_pos.translated(-window.width(), 0);
464511
}
512+
if (adjusted_pos.y() + window.height() >= WSScreen::the().height() - margin) {
513+
adjusted_pos = adjusted_pos.translated(0, -window.height());
514+
}
515+
516+
if (adjusted_pos.y() < WSMenuManager::the().menubar_rect().height())
517+
adjusted_pos.set_y(WSMenuManager::the().menubar_rect().height());
465518

466519
window.move_to(adjusted_pos);
467520
window.set_visible(true);

Servers/WindowServer/WSMenu.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ class WSMenu final : public CObject {
8484
bool is_window_menu_open() { return m_is_window_menu_open; }
8585
void set_window_menu_open(bool is_open) { m_is_window_menu_open = is_open; }
8686

87-
int width() const;
88-
int height() const;
87+
int content_width() const;
8988

9089
int item_height() const { return 20; }
9190
int frame_thickness() const { return 3; }
@@ -112,9 +111,15 @@ class WSMenu final : public CObject {
112111

113112
void redraw_if_theme_changed();
114113

114+
bool is_scrollable() const { return m_scrollable; }
115+
int scroll_offset() const { return m_scroll_offset; }
116+
115117
private:
116118
virtual void event(CEvent&) override;
117119

120+
void handle_hover_event(const WSMouseEvent&);
121+
int visible_item_count() const;
122+
118123
int item_index_at(const Point&);
119124
int padding_between_text_and_shortcut() const { return 50; }
120125
void did_activate(WSMenuItem&);
@@ -137,4 +142,8 @@ class WSMenu final : public CObject {
137142
int m_theme_index_at_last_paint { -1 };
138143
int m_hovered_item_index { -1 };
139144
bool m_in_submenu { false };
145+
146+
bool m_scrollable { false };
147+
int m_scroll_offset { 0 };
148+
int m_max_scroll_offset { 0 };
140149
};

Servers/WindowServer/WSMenuItem.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,10 @@ WSMenu* WSMenuItem::submenu()
7676
return m_menu.client()->find_menu_by_id(m_submenu_id);
7777
return WSMenuManager::the().find_internal_menu_by_id(m_submenu_id);
7878
}
79+
80+
Rect WSMenuItem::rect() const
81+
{
82+
if (!m_menu.is_scrollable())
83+
return m_rect;
84+
return m_rect.translated(0, m_menu.item_height() - (m_menu.scroll_offset() * m_menu.item_height()));
85+
}

Servers/WindowServer/WSMenuItem.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class WSMenuItem {
6363
void set_shortcut_text(const String& text) { m_shortcut_text = text; }
6464

6565
void set_rect(const Rect& rect) { m_rect = rect; }
66-
Rect rect() const { return m_rect; }
66+
Rect rect() const;
6767

6868
unsigned identifier() const { return m_identifier; }
6969

0 commit comments

Comments
 (0)