Serenity Operating System
1/*
2 * Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <LibGUI/Painter.h>
9#include <LibGUI/Tray.h>
10#include <LibGfx/Font/Font.h>
11#include <LibGfx/Palette.h>
12#include <LibGfx/StylePainter.h>
13
14REGISTER_WIDGET(GUI, Tray);
15
16namespace GUI {
17
18Tray::Tray()
19{
20 set_fill_with_background_color(true);
21 set_background_role(Gfx::ColorRole::Tray);
22 set_focus_policy(GUI::FocusPolicy::TabFocus);
23}
24
25Gfx::IntRect Tray::Item::rect(Tray const& tray) const
26{
27 int item_height = tray.font().pixel_size_rounded_up() + 12;
28 return Gfx::IntRect {
29 tray.frame_thickness(),
30 tray.frame_thickness() + static_cast<int>(index) * item_height,
31 tray.frame_inner_rect().width(),
32 item_height,
33 };
34}
35
36size_t Tray::add_item(DeprecatedString text, RefPtr<Gfx::Bitmap const> bitmap, DeprecatedString custom_data)
37{
38 auto new_index = m_items.size();
39
40 m_items.append(Item {
41 .text = move(text),
42 .bitmap = move(bitmap),
43 .custom_data = move(custom_data),
44 .index = new_index,
45 });
46 update();
47
48 return new_index;
49}
50
51void Tray::set_item_checked(size_t index, bool checked)
52{
53 if (checked) {
54 m_checked_item_index = index;
55 } else {
56 if (m_checked_item_index == index)
57 m_checked_item_index = {};
58 }
59 update();
60}
61
62void Tray::paint_event(GUI::PaintEvent& event)
63{
64 GUI::Frame::paint_event(event);
65
66 GUI::Painter painter(*this);
67 painter.add_clip_rect(event.rect());
68
69 for (auto& item : m_items) {
70 auto rect = item.rect(*this);
71 bool is_pressed = item.index == m_pressed_item_index;
72 bool is_hovered = item.index == m_hovered_item_index;
73 bool is_checked = item.index == m_checked_item_index;
74 Gfx::StylePainter::paint_button(painter, rect, palette(), Gfx::ButtonStyle::Tray, is_pressed && is_hovered, is_hovered, is_checked, is_enabled());
75
76 Gfx::IntRect icon_rect {
77 rect.x() + 4,
78 0,
79 16,
80 16,
81 };
82 icon_rect.center_vertically_within(rect);
83
84 Gfx::IntRect text_rect {
85 icon_rect.right() + 5,
86 rect.y(),
87 rect.width(),
88 rect.height(),
89 };
90 text_rect.intersect(rect);
91
92 if (is_pressed && is_hovered) {
93 icon_rect.translate_by(1, 1);
94 text_rect.translate_by(1, 1);
95 }
96
97 if (item.bitmap) {
98 if (is_hovered)
99 painter.blit_brightened(icon_rect.location(), *item.bitmap, item.bitmap->rect());
100 else
101 painter.blit(icon_rect.location(), *item.bitmap, item.bitmap->rect());
102 }
103
104 auto const& font = is_checked ? this->font().bold_variant() : this->font();
105 painter.draw_text(text_rect, item.text, font, Gfx::TextAlignment::CenterLeft, palette().tray_text());
106 }
107}
108
109void Tray::mousemove_event(GUI::MouseEvent& event)
110{
111 auto* hovered_item = item_at(event.position());
112 if (!hovered_item) {
113 if (m_hovered_item_index.has_value())
114 update();
115 m_hovered_item_index = {};
116 return;
117 }
118 if (m_hovered_item_index != hovered_item->index) {
119 m_hovered_item_index = hovered_item->index;
120 update();
121 }
122}
123
124void Tray::mousedown_event(GUI::MouseEvent& event)
125{
126 if (event.button() != GUI::MouseButton::Primary)
127 return;
128
129 auto* pressed_item = item_at(event.position());
130 if (!pressed_item)
131 return;
132
133 if (m_pressed_item_index != pressed_item->index) {
134 m_pressed_item_index = pressed_item->index;
135 update();
136 }
137}
138
139void Tray::mouseup_event(GUI::MouseEvent& event)
140{
141 if (event.button() != GUI::MouseButton::Primary)
142 return;
143
144 if (auto* pressed_item = item_at(event.position()); pressed_item && m_pressed_item_index == pressed_item->index) {
145 on_item_activation(pressed_item->custom_data);
146 }
147
148 m_pressed_item_index = {};
149 update();
150}
151
152void Tray::leave_event(Core::Event&)
153{
154 m_hovered_item_index = {};
155 update();
156}
157
158Tray::Item* Tray::item_at(Gfx::IntPoint position)
159{
160 for (auto& item : m_items) {
161 if (item.rect(*this).contains(position))
162 return &item;
163 }
164 return nullptr;
165}
166
167void Tray::focusin_event(GUI::FocusEvent&)
168{
169 if (m_items.is_empty())
170 return;
171 m_hovered_item_index = 0;
172 update();
173}
174
175void Tray::focusout_event(GUI::FocusEvent&)
176{
177 if (m_items.is_empty())
178 return;
179 m_hovered_item_index = {};
180 update();
181}
182
183void Tray::keydown_event(GUI::KeyEvent& event)
184{
185 if (m_items.is_empty() || event.modifiers())
186 return Frame::keydown_event(event);
187
188 if (event.key() == KeyCode::Key_Down) {
189 if (!m_hovered_item_index.has_value())
190 m_hovered_item_index = 0;
191 else
192 m_hovered_item_index = (*m_hovered_item_index + 1) % m_items.size();
193 update();
194 return;
195 }
196
197 if (event.key() == KeyCode::Key_Up) {
198 if (!m_hovered_item_index.has_value() || m_hovered_item_index == 0u)
199 m_hovered_item_index = m_items.size() - 1;
200 else
201 m_hovered_item_index = *m_hovered_item_index - 1;
202 update();
203 return;
204 }
205
206 if (event.key() == KeyCode::Key_Return) {
207 if (m_hovered_item_index.has_value())
208 on_item_activation(m_items[*m_hovered_item_index].custom_data);
209 return;
210 }
211
212 Frame::keydown_event(event);
213}
214
215}