Serenity Operating System
at master 220 lines 6.7 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include <AK/URL.h> 10#include <AK/Vector.h> 11#include <LibGUI/Painter.h> 12#include <LibGUI/TextBox.h> 13#include <LibGfx/Palette.h> 14#include <LibGfx/TextAttributes.h> 15 16REGISTER_WIDGET(GUI, TextBox) 17REGISTER_WIDGET(GUI, PasswordBox) 18REGISTER_WIDGET(GUI, UrlBox) 19 20namespace GUI { 21 22TextBox::TextBox() 23 : TextEditor(TextEditor::SingleLine) 24{ 25 set_min_size({ 40, 22 }); 26 set_preferred_size({ SpecialDimension::OpportunisticGrow, 22 }); 27} 28 29void TextBox::keydown_event(GUI::KeyEvent& event) 30{ 31 TextEditor::keydown_event(event); 32 33 if (event.key() == Key_Up) { 34 if (on_up_pressed) 35 on_up_pressed(); 36 37 if (has_no_history() || !can_go_backwards_in_history()) 38 return; 39 40 if (m_history_index >= static_cast<int>(m_history.size())) 41 m_saved_input = text(); 42 43 m_history_index--; 44 set_text(m_history[m_history_index]); 45 } else if (event.key() == Key_Down) { 46 if (on_down_pressed) 47 on_down_pressed(); 48 49 if (has_no_history()) 50 return; 51 52 if (can_go_forwards_in_history()) { 53 m_history_index++; 54 set_text(m_history[m_history_index]); 55 } else if (m_history_index < static_cast<int>(m_history.size())) { 56 m_history_index++; 57 set_text(m_saved_input); 58 } 59 } 60} 61 62void TextBox::add_current_text_to_history() 63{ 64 if (!m_history_enabled) 65 return; 66 67 auto input = text(); 68 if (m_history.is_empty() || m_history.last() != input) 69 add_input_to_history(input); 70 m_history_index = static_cast<int>(m_history.size()); 71 m_saved_input = {}; 72} 73 74void TextBox::add_input_to_history(DeprecatedString input) 75{ 76 m_history.append(move(input)); 77 m_history_index++; 78} 79 80constexpr u32 password_box_substitution_code_point = '*'; 81 82PasswordBox::PasswordBox() 83 : TextBox() 84{ 85 set_substitution_code_point(password_box_substitution_code_point); 86 set_text_is_secret(true); 87 REGISTER_BOOL_PROPERTY("show_reveal_button", is_showing_reveal_button, set_show_reveal_button); 88} 89 90Gfx::IntRect PasswordBox::reveal_password_button_rect() const 91{ 92 constexpr i32 button_box_margin = 3; 93 auto button_box_size = height() - button_box_margin - button_box_margin; 94 return { width() - button_box_size - button_box_margin, button_box_margin, button_box_size, button_box_size }; 95} 96 97void PasswordBox::paint_event(PaintEvent& event) 98{ 99 TextBox::paint_event(event); 100 101 if (is_showing_reveal_button()) { 102 auto button_rect = reveal_password_button_rect(); 103 104 Painter painter(*this); 105 painter.add_clip_rect(event.rect()); 106 107 auto icon_color = palette().button_text(); 108 if (substitution_code_point().has_value()) 109 icon_color = palette().disabled_text_front(); 110 111 i32 dot_indicator_padding = height() / 5; 112 113 Gfx::IntRect dot_indicator_rect = { button_rect.x() + dot_indicator_padding, button_rect.y() + dot_indicator_padding, button_rect.width() - dot_indicator_padding * 2, button_rect.height() - dot_indicator_padding * 2 }; 114 painter.fill_ellipse(dot_indicator_rect, icon_color); 115 116 Gfx::IntPoint arc_start_point { dot_indicator_rect.x() - dot_indicator_padding / 2, dot_indicator_rect.y() + dot_indicator_padding / 2 }; 117 Gfx::IntPoint arc_end_point = { dot_indicator_rect.top_right().x() + dot_indicator_padding / 2, dot_indicator_rect.top_right().y() + dot_indicator_padding / 2 }; 118 Gfx::IntPoint arc_center_point = { dot_indicator_rect.center().x(), dot_indicator_rect.top() - dot_indicator_padding }; 119 painter.draw_quadratic_bezier_curve(arc_center_point, arc_start_point, arc_end_point, icon_color, 1); 120 } 121} 122 123void PasswordBox::mousedown_event(GUI::MouseEvent& event) 124{ 125 if (is_showing_reveal_button() && reveal_password_button_rect().contains(event.position())) { 126 Optional<u32> next_substitution_code_point; 127 if (!substitution_code_point().has_value()) 128 next_substitution_code_point = password_box_substitution_code_point; 129 set_substitution_code_point(next_substitution_code_point); 130 } else { 131 TextBox::mousedown_event(event); 132 } 133} 134 135UrlBox::UrlBox() 136 : TextBox() 137{ 138 set_auto_focusable(false); 139 on_change = [this] { 140 highlight_url(); 141 }; 142} 143 144void UrlBox::focusout_event(GUI::FocusEvent& event) 145{ 146 set_focus_transition(true); 147 148 highlight_url(); 149 TextBox::focusout_event(event); 150} 151 152void UrlBox::focusin_event(GUI::FocusEvent& event) 153{ 154 highlight_url(); 155 TextBox::focusin_event(event); 156} 157 158void UrlBox::mousedown_event(GUI::MouseEvent& event) 159{ 160 if (is_displayonly()) 161 return; 162 163 if (event.button() != MouseButton::Primary) 164 return; 165 166 if (is_focus_transition()) { 167 TextBox::select_current_line(); 168 169 set_focus_transition(false); 170 } else { 171 TextBox::mousedown_event(event); 172 } 173} 174 175void UrlBox::highlight_url() 176{ 177 auto url = AK::URL::create_with_url_or_path(text()); 178 Vector<GUI::TextDocumentSpan> spans; 179 180 if (url.is_valid() && !is_focused()) { 181 if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "gemini") { 182 auto host_start = url.scheme().length() + 3; 183 auto host_length = url.host().length(); 184 185 // FIXME: Maybe add a generator to use https://publicsuffix.org/list/public_suffix_list.dat 186 // for now just highlight the whole host 187 188 Gfx::TextAttributes default_format; 189 default_format.color = palette().color(Gfx::ColorRole::PlaceholderText); 190 spans.append({ 191 { { 0, 0 }, { 0, host_start } }, 192 default_format, 193 }); 194 195 Gfx::TextAttributes host_format; 196 host_format.color = palette().color(Gfx::ColorRole::BaseText); 197 spans.append({ 198 { { 0, host_start }, { 0, host_start + host_length } }, 199 host_format, 200 }); 201 202 spans.append({ 203 { { 0, host_start + host_length }, { 0, text().length() } }, 204 default_format, 205 }); 206 } else if (url.scheme() == "file") { 207 Gfx::TextAttributes scheme_format; 208 scheme_format.color = palette().color(Gfx::ColorRole::PlaceholderText); 209 spans.append({ 210 { { 0, 0 }, { 0, url.scheme().length() + 3 } }, 211 scheme_format, 212 }); 213 } 214 } 215 216 document().set_spans(0, move(spans)); 217 update(); 218} 219 220}