Serenity Operating System
1/*
2 * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/StringBuilder.h>
8#include <LibGUI/Action.h>
9#include <LibGUI/ActionGroup.h>
10#include <LibGUI/Button.h>
11#include <LibGUI/Menu.h>
12#include <LibGUI/Painter.h>
13#include <LibGUI/Window.h>
14#include <LibGfx/Font/Font.h>
15#include <LibGfx/Palette.h>
16#include <LibGfx/StylePainter.h>
17
18REGISTER_WIDGET(GUI, Button)
19REGISTER_WIDGET(GUI, DialogButton)
20
21namespace GUI {
22
23Button::Button(String text)
24 : AbstractButton(move(text))
25{
26 set_min_size({ 40, SpecialDimension::Shrink });
27 set_preferred_size({ SpecialDimension::OpportunisticGrow, SpecialDimension::Shrink });
28 set_focus_policy(GUI::FocusPolicy::StrongFocus);
29
30 on_focus_change = [this](bool has_focus, auto) {
31 if (!is_default())
32 return;
33 if (!has_focus && is<Button>(window()->focused_widget()))
34 m_another_button_has_focus = true;
35 else
36 m_another_button_has_focus = false;
37 update();
38 };
39
40 REGISTER_ENUM_PROPERTY(
41 "button_style", button_style, set_button_style, Gfx::ButtonStyle,
42 { Gfx::ButtonStyle::Normal, "Normal" },
43 { Gfx::ButtonStyle::Coolbar, "Coolbar" });
44
45 REGISTER_WRITE_ONLY_STRING_PROPERTY("icon", set_icon_from_path);
46 REGISTER_BOOL_PROPERTY("default", is_default, set_default);
47}
48
49Button::~Button()
50{
51 if (m_action)
52 m_action->unregister_button({}, *this);
53}
54
55void Button::paint_event(PaintEvent& event)
56{
57 Painter painter(*this);
58 painter.add_clip_rect(event.rect());
59
60 bool paint_pressed = is_being_pressed() || m_mimic_pressed || (m_menu && m_menu->is_visible());
61
62 Gfx::StylePainter::paint_button(painter, rect(), palette(), m_button_style, paint_pressed, is_hovered(), is_checked(), is_enabled(), is_focused(), is_default() && !another_button_has_focus());
63
64 if (text().is_empty() && !m_icon)
65 return;
66
67 auto content_rect = rect().shrunken(8, 2);
68 auto icon_location = m_icon ? content_rect.center().translated(-(m_icon->width() / 2), -(m_icon->height() / 2)) : Gfx::IntPoint();
69 if (m_icon && !text().is_empty())
70 icon_location.set_x(content_rect.x());
71
72 if (paint_pressed || is_checked()) {
73 painter.translate(1, 1);
74 } else if (m_icon && is_enabled() && is_hovered() && button_style() == Gfx::ButtonStyle::Coolbar) {
75 auto shadow_color = palette().button().darkened(0.7f);
76 painter.blit_filtered(icon_location.translated(1, 1), *m_icon, m_icon->rect(), [&shadow_color](auto) {
77 return shadow_color;
78 });
79 icon_location.translate_by(-1, -1);
80 }
81
82 if (m_icon) {
83 auto solid_color = m_icon->solid_color(60);
84 bool should_invert_icon = false;
85 if (solid_color.has_value()) {
86 auto contrast_ratio = palette().button().contrast_ratio(*solid_color);
87 // Note: 4.5 is the minimum recommended contrast ratio for text on the web:
88 // (https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast)
89 // Reusing that threshold here as it seems to work reasonably well.
90 should_invert_icon = contrast_ratio < 4.5f && contrast_ratio < palette().button().contrast_ratio(solid_color->inverted());
91 }
92 auto icon = should_invert_icon ? m_icon->inverted().release_value_but_fixme_should_propagate_errors() : NonnullRefPtr { *m_icon };
93 if (is_enabled()) {
94 if (is_hovered())
95 painter.blit_brightened(icon_location, *icon, icon->rect());
96 else
97 painter.blit(icon_location, *icon, icon->rect());
98 } else {
99 painter.blit_disabled(icon_location, *icon, icon->rect(), palette());
100 }
101 }
102 auto& font = is_checked() ? this->font().bold_variant() : this->font();
103 if (m_icon && !text().is_empty()) {
104 content_rect.translate_by(m_icon->width() + icon_spacing(), 0);
105 content_rect.set_width(content_rect.width() - m_icon->width() - icon_spacing());
106 }
107
108 Gfx::IntRect text_rect { 0, 0, static_cast<int>(ceilf(font.width(text()))), font.pixel_size_rounded_up() };
109 if (text_rect.width() > content_rect.width())
110 text_rect.set_width(content_rect.width());
111 text_rect.align_within(content_rect, text_alignment());
112 paint_text(painter, text_rect, font, text_alignment());
113
114 if (is_focused()) {
115 Gfx::IntRect focus_rect;
116 if (m_icon && !text().is_empty())
117 focus_rect = text_rect.inflated(4, 4);
118 else
119 focus_rect = rect().shrunken(8, 8);
120 painter.draw_focus_rect(focus_rect, palette().focus_outline());
121 }
122}
123
124void Button::click(unsigned modifiers)
125{
126 if (!is_enabled())
127 return;
128
129 NonnullRefPtr protector = *this;
130
131 if (is_checkable()) {
132 if (is_checked() && !is_uncheckable())
133 return;
134 set_checked(!is_checked());
135 }
136
137 mimic_pressed();
138
139 if (on_click)
140 on_click(modifiers);
141 if (m_action)
142 m_action->activate(this);
143}
144
145void Button::double_click(unsigned int modifiers)
146{
147 if (on_double_click)
148 on_double_click(modifiers);
149}
150
151void Button::middle_mouse_click(unsigned int modifiers)
152{
153 if (!is_enabled())
154 return;
155
156 NonnullRefPtr protector = *this;
157
158 if (on_middle_mouse_click)
159 on_middle_mouse_click(modifiers);
160}
161
162void Button::context_menu_event(ContextMenuEvent& context_menu_event)
163{
164 if (!is_enabled())
165 return;
166 if (on_context_menu_request)
167 on_context_menu_request(context_menu_event);
168}
169
170void Button::set_action(Action& action)
171{
172 m_action = action;
173 action.register_button({}, *this);
174 set_visible(action.is_visible());
175 set_enabled(action.is_enabled());
176 set_checkable(action.is_checkable());
177 if (action.is_checkable())
178 set_checked(action.is_checked());
179}
180
181void Button::set_icon(RefPtr<Gfx::Bitmap const> icon)
182{
183 if (m_icon == icon)
184 return;
185 m_icon = move(icon);
186 update();
187}
188
189void Button::set_icon_from_path(DeprecatedString const& path)
190{
191 auto maybe_bitmap = Gfx::Bitmap::load_from_file(path);
192 if (maybe_bitmap.is_error()) {
193 dbgln("Unable to load bitmap `{}` for button icon", path);
194 return;
195 }
196 set_icon(maybe_bitmap.release_value());
197}
198
199bool Button::is_uncheckable() const
200{
201 if (!m_action)
202 return true;
203 if (!m_action->group())
204 return true;
205 return m_action->group()->is_unchecking_allowed();
206}
207
208void Button::set_menu(RefPtr<GUI::Menu> menu)
209{
210 if (m_menu == menu)
211 return;
212 if (m_menu)
213 m_menu->on_visibility_change = nullptr;
214 m_menu = menu;
215 if (m_menu) {
216 m_menu->on_visibility_change = [&](bool) {
217 update();
218 };
219 }
220}
221
222void Button::mousedown_event(MouseEvent& event)
223{
224 if (m_menu) {
225 m_menu->popup(screen_relative_rect().bottom_left(), {}, rect());
226 update();
227 return;
228 }
229 AbstractButton::mousedown_event(event);
230}
231
232void Button::mousemove_event(MouseEvent& event)
233{
234 if (m_menu) {
235 return;
236 }
237 AbstractButton::mousemove_event(event);
238}
239
240bool Button::is_default() const
241{
242 if (!window())
243 return false;
244 return this == window()->default_return_key_widget();
245}
246
247void Button::set_default(bool default_button)
248{
249 deferred_invoke([this, default_button] {
250 VERIFY(window());
251 window()->set_default_return_key_widget(default_button ? this : nullptr);
252 });
253}
254
255void Button::mimic_pressed()
256{
257 if (!is_being_pressed() && !was_being_pressed()) {
258 m_mimic_pressed = true;
259
260 stop_timer();
261 start_timer(80, Core::TimerShouldFireWhenNotVisible::Yes);
262
263 update();
264 }
265}
266
267void Button::timer_event(Core::TimerEvent&)
268{
269 if (m_mimic_pressed) {
270 m_mimic_pressed = false;
271
272 update();
273 }
274}
275
276Optional<UISize> Button::calculated_min_size() const
277{
278 int width = 0;
279 int height = 0;
280
281 if (!text().is_empty()) {
282 auto& font = this->font();
283 width = static_cast<int>(ceilf(font.width(text()))) + 2;
284 height = font.pixel_size_rounded_up() + 4; // FIXME: Use actual maximum total height
285 }
286
287 if (m_icon) {
288 height += max(height, m_icon->height());
289 width += m_icon->width() + icon_spacing();
290 }
291
292 width += 8;
293 height += 4;
294
295 height = max(22, height);
296
297 return UISize(width, height);
298}
299
300}