Serenity Operating System
at master 300 lines 8.5 kB view raw
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}