Serenity Operating System
at master 598 lines 21 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org> 4 * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> 5 * Copyright (c) 2022, the SerenityOS developers. 6 * 7 * SPDX-License-Identifier: BSD-2-Clause 8 */ 9 10#include "GlyphMapWidget.h" 11#include <LibGUI/Painter.h> 12#include <LibGfx/Font/BitmapFont.h> 13#include <LibGfx/Font/Emoji.h> 14#include <LibGfx/Palette.h> 15 16REGISTER_WIDGET(GUI, GlyphMapWidget); 17 18namespace GUI { 19 20GlyphMapWidget::Selection GlyphMapWidget::Selection::normalized() const 21{ 22 if (m_size > 0) 23 return *this; 24 return { m_start + m_size, -m_size + 1 }; 25} 26 27void GlyphMapWidget::Selection::resize_by(int i) 28{ 29 m_size += i; 30 if (m_size == 0) { 31 if (i < 0) 32 m_size--; 33 else 34 m_size++; 35 } 36} 37 38bool GlyphMapWidget::Selection::contains(int i) const 39{ 40 auto this_normalized = normalized(); 41 return i >= this_normalized.m_start && i < this_normalized.m_start + this_normalized.m_size; 42} 43 44void GlyphMapWidget::Selection::extend_to(int glyph) 45{ 46 m_size = glyph - m_start; 47 if (m_size >= 0) 48 m_size++; 49} 50 51GlyphMapWidget::GlyphMapWidget() 52{ 53 set_focus_policy(FocusPolicy::StrongFocus); 54 horizontal_scrollbar().set_visible(false); 55 did_change_font(); 56 set_active_glyph('A'); 57} 58 59void GlyphMapWidget::resize_event(ResizeEvent& event) 60{ 61 recalculate_content_size(); 62 AbstractScrollableWidget::resize_event(event); 63} 64 65void GlyphMapWidget::set_active_glyph(int glyph, ShouldResetSelection should_reset_selection) 66{ 67 if (m_active_glyph == glyph && should_reset_selection == ShouldResetSelection::No) 68 return; 69 m_active_glyph = glyph; 70 if (should_reset_selection == ShouldResetSelection::Yes) { 71 m_selection.set_start(glyph); 72 m_selection.set_size(1); 73 } 74 if (on_active_glyph_changed) 75 on_active_glyph_changed(glyph); 76 update(); 77} 78 79void GlyphMapWidget::set_selection(int start, int size, Optional<u32> active_glyph) 80{ 81 m_selection.set_start(start); 82 m_selection.set_size(size); 83 if (active_glyph.has_value()) 84 set_active_glyph(active_glyph.value(), ShouldResetSelection::No); 85} 86 87void GlyphMapWidget::restore_selection(int start, int size, int active_glyph) 88{ 89 if (start == active_glyph && size > 1) { 90 start = active_glyph + size - 1; 91 size = -size + 1; 92 } 93 m_selection.set_start(start); 94 m_selection.set_size(size); 95 set_active_glyph(active_glyph, ShouldResetSelection::No); 96} 97 98Gfx::IntRect GlyphMapWidget::get_outer_rect(int glyph) const 99{ 100 glyph -= m_active_range.first; 101 int row = glyph / columns(); 102 int column = glyph % columns(); 103 return Gfx::IntRect { 104 column * (font().max_glyph_width() + m_horizontal_spacing), 105 row * (font().pixel_size_rounded_up() + m_vertical_spacing), 106 font().max_glyph_width() + m_horizontal_spacing, 107 font().pixel_size_rounded_up() + m_vertical_spacing 108 } 109 .translated(frame_thickness() - horizontal_scrollbar().value(), frame_thickness() - vertical_scrollbar().value()); 110} 111 112void GlyphMapWidget::update_glyph(int glyph) 113{ 114 set_glyph_modified(glyph, true); 115 update(get_outer_rect(glyph)); 116} 117 118void GlyphMapWidget::paint_event(PaintEvent& event) 119{ 120 Frame::paint_event(event); 121 122 Painter painter(*this); 123 painter.add_clip_rect(widget_inner_rect()); 124 painter.add_clip_rect(event.rect()); 125 126 painter.set_font(font()); 127 painter.fill_rect(widget_inner_rect(), palette().window().darkened(0.8f)); 128 129 auto first_row = vertical_scrollbar().value() / vertical_scrollbar().step(); 130 auto first_glyph = first_row * columns() + m_active_range.first; 131 auto last_glyph = m_active_range.last; 132 133 for (u32 glyph = first_glyph; glyph <= first_glyph + m_visible_glyphs && glyph <= last_glyph; ++glyph) { 134 Gfx::IntRect outer_rect = get_outer_rect(glyph); 135 Gfx::IntRect inner_rect( 136 outer_rect.x() + m_horizontal_spacing / 2, 137 outer_rect.y() + m_vertical_spacing / 2, 138 font().max_glyph_width(), 139 font().pixel_size_rounded_up()); 140 if (m_selection.contains(glyph)) { 141 painter.fill_rect(outer_rect, is_focused() ? palette().selection() : palette().inactive_selection()); 142 if (font().contains_glyph(glyph)) 143 painter.draw_glyph(inner_rect.location(), glyph, is_focused() ? palette().selection_text() : palette().inactive_selection_text()); 144 else if (auto* emoji = Gfx::Emoji::emoji_for_code_point(glyph); emoji && m_show_system_emoji) 145 painter.draw_emoji(inner_rect.location(), *emoji, font()); 146 } else if (font().contains_glyph(glyph)) { 147 if (m_highlight_modifications && m_modified_glyphs.contains(glyph)) { 148 if (m_original_font->contains_glyph(glyph)) { 149 // Modified 150 if (palette().is_dark()) 151 painter.fill_rect(outer_rect, Gfx::Color { 0, 65, 159 }); 152 else 153 painter.fill_rect(outer_rect, Gfx::Color { 138, 185, 252 }); 154 } else { 155 // Newly created 156 if (palette().is_dark()) 157 painter.fill_rect(outer_rect, Gfx::Color { 8, 127, 0 }); 158 else 159 painter.fill_rect(outer_rect, Gfx::Color { 133, 251, 116 }); 160 } 161 } else { 162 painter.fill_rect(outer_rect, palette().base()); 163 } 164 painter.draw_glyph(inner_rect.location(), glyph, palette().base_text()); 165 } else if (auto* emoji = Gfx::Emoji::emoji_for_code_point(glyph); emoji && m_show_system_emoji) { 166 painter.draw_emoji(inner_rect.location(), *emoji, font()); 167 } else { 168 if (m_highlight_modifications && m_original_font->contains_glyph(glyph)) { 169 // Deleted 170 if (palette().is_dark()) 171 painter.fill_rect(outer_rect, Gfx::Color { 127, 0, 0 }); 172 else 173 painter.fill_rect(outer_rect, Gfx::Color { 255, 150, 150 }); 174 } else { 175 painter.fill_rect(outer_rect, palette().window()); 176 } 177 } 178 } 179 painter.draw_focus_rect(get_outer_rect(m_active_glyph), palette().focus_outline()); 180} 181 182Optional<int> GlyphMapWidget::glyph_at_position(Gfx::IntPoint position) const 183{ 184 Gfx::IntPoint map_offset { frame_thickness() - horizontal_scrollbar().value(), frame_thickness() - vertical_scrollbar().value() }; 185 auto map_position = position - map_offset; 186 auto col = (map_position.x() - 1) / ((font().max_glyph_width() + m_horizontal_spacing)); 187 auto row = (map_position.y() - 1) / ((font().pixel_size_rounded_up() + m_vertical_spacing)); 188 auto glyph = row * columns() + col + m_active_range.first; 189 if (row >= 0 && row < rows() && col >= 0 && col < columns() && glyph < m_glyph_count + m_active_range.first) 190 return glyph; 191 192 return {}; 193} 194 195int GlyphMapWidget::glyph_at_position_clamped(Gfx::IntPoint position) const 196{ 197 Gfx::IntPoint map_offset { frame_thickness() - horizontal_scrollbar().value(), frame_thickness() - vertical_scrollbar().value() }; 198 auto map_position = position - map_offset; 199 auto col = clamp((map_position.x() - 1) / ((font().max_glyph_width() + m_horizontal_spacing)), 0, columns() - 1); 200 auto row = clamp((map_position.y() - 1) / ((font().pixel_size_rounded_up() + m_vertical_spacing)), 0, rows() - 1); 201 auto glyph = row * columns() + col + m_active_range.first; 202 if (row == rows() - 1) 203 glyph = min(glyph, m_glyph_count + m_active_range.first - 1); 204 return glyph; 205} 206 207void GlyphMapWidget::context_menu_event(GUI::ContextMenuEvent& event) 208{ 209 if (on_context_menu_request) 210 on_context_menu_request(event); 211} 212 213void GlyphMapWidget::mousedown_event(MouseEvent& event) 214{ 215 if (event.button() == MouseButton::Secondary) 216 return; 217 218 if (auto maybe_glyph = glyph_at_position(event.position()); maybe_glyph.has_value()) { 219 auto glyph = maybe_glyph.value(); 220 if (event.shift()) 221 m_selection.extend_to(glyph); 222 m_in_drag_select = true; 223 set_active_glyph(glyph, event.shift() ? ShouldResetSelection::No : ShouldResetSelection::Yes); 224 } 225} 226 227void GlyphMapWidget::mouseup_event(GUI::MouseEvent& event) 228{ 229 if (event.button() == MouseButton::Secondary) 230 return; 231 232 if (!m_in_drag_select) 233 return; 234 auto constrained = event.position().constrained(widget_inner_rect()); 235 if (auto maybe_glyph = glyph_at_position(constrained); maybe_glyph.has_value()) { 236 auto glyph = maybe_glyph.value(); 237 m_selection.extend_to(glyph); 238 set_active_glyph(glyph, ShouldResetSelection::No); 239 } 240 m_in_drag_select = false; 241} 242 243void GlyphMapWidget::mousemove_event(GUI::MouseEvent& event) 244{ 245 m_last_mousemove_position = event.position(); 246 if (m_in_drag_select) { 247 auto constrained = event.position().constrained(widget_inner_rect()); 248 auto glyph = glyph_at_position_clamped(constrained); 249 m_selection.extend_to(glyph); 250 set_active_glyph(glyph, ShouldResetSelection::No); 251 scroll_to_glyph(glyph); 252 update(); 253 } 254} 255 256void GlyphMapWidget::automatic_scrolling_timer_did_fire() 257{ 258 if (!m_in_drag_select) { 259 set_automatic_scrolling_timer_active(false); 260 return; 261 } 262 auto glyph = glyph_at_position_clamped(m_last_mousemove_position); 263 m_selection.extend_to(glyph); 264 set_active_glyph(glyph, ShouldResetSelection::No); 265 scroll_to_glyph(glyph); 266 update(); 267} 268 269void GlyphMapWidget::doubleclick_event(MouseEvent& event) 270{ 271 if (on_glyph_double_clicked) { 272 if (auto maybe_glyph = glyph_at_position(event.position()); maybe_glyph.has_value()) 273 on_glyph_double_clicked(maybe_glyph.value()); 274 } 275} 276 277void GlyphMapWidget::keydown_event(KeyEvent& event) 278{ 279 if (event.key() == KeyCode::Key_Tab) { 280 AbstractScrollableWidget::keydown_event(event); 281 return; 282 } 283 284 if (event.key() == KeyCode::Key_Escape) { 285 m_selection.set_size(1); 286 m_selection.set_start(m_active_glyph); 287 if (on_escape_pressed) 288 on_escape_pressed(); 289 return; 290 } 291 292 if (!event.modifiers() && event.is_arrow_key()) { 293 m_selection.set_size(1); 294 m_selection.set_start(m_active_glyph); 295 } 296 297 if (event.shift() && event.is_arrow_key()) { 298 auto resizing_end = m_selection.start() + m_selection.size() - (m_selection.size() > 0 ? 1 : 0); 299 set_active_glyph(resizing_end, ShouldResetSelection::No); 300 scroll_to_glyph(resizing_end); 301 } 302 303 int first_glyph = m_active_range.first; 304 int last_glyph = m_active_range.last; 305 auto selection = m_selection.normalized(); 306 307 if (event.key() == KeyCode::Key_Up) { 308 if (m_active_glyph - m_columns < first_glyph) 309 return; 310 if (event.ctrl() && selection.start() - m_columns < first_glyph) 311 return; 312 if (event.shift()) 313 m_selection.extend_to(m_active_glyph - m_columns); 314 else 315 m_selection.set_start(m_selection.start() - m_columns); 316 set_active_glyph(m_active_glyph - m_columns, ShouldResetSelection::No); 317 scroll_to_glyph(m_active_glyph); 318 return; 319 } 320 321 if (event.key() == KeyCode::Key_Down) { 322 if (m_active_glyph + m_columns > last_glyph) 323 return; 324 if (event.ctrl() && selection.start() + selection.size() - 1 + m_columns > last_glyph) 325 return; 326 if (event.shift()) 327 m_selection.extend_to(m_active_glyph + m_columns); 328 else 329 m_selection.set_start(m_selection.start() + m_columns); 330 set_active_glyph(m_active_glyph + m_columns, ShouldResetSelection::No); 331 scroll_to_glyph(m_active_glyph); 332 return; 333 } 334 335 if (event.key() == KeyCode::Key_Left) { 336 if (m_active_glyph - 1 < first_glyph) 337 return; 338 if (event.ctrl() && selection.start() - 1 < first_glyph) 339 return; 340 if (event.shift()) 341 m_selection.resize_by(-1); 342 else 343 m_selection.set_start(m_selection.start() - 1); 344 set_active_glyph(m_active_glyph - 1, ShouldResetSelection::No); 345 scroll_to_glyph(m_active_glyph); 346 return; 347 } 348 349 if (event.key() == KeyCode::Key_Right) { 350 if (m_active_glyph + 1 > last_glyph) 351 return; 352 if (event.ctrl() && selection.start() + selection.size() > last_glyph) 353 return; 354 if (event.shift()) 355 m_selection.resize_by(1); 356 else 357 m_selection.set_start(m_selection.start() + 1); 358 set_active_glyph(m_active_glyph + 1, ShouldResetSelection::No); 359 scroll_to_glyph(m_active_glyph); 360 return; 361 } 362 363 if (event.key() == KeyCode::Key_Home) { 364 if (event.alt()) { 365 set_active_glyph(first_glyph); 366 scroll_to_glyph(m_active_glyph); 367 return; 368 } 369 if (event.ctrl() && event.shift()) { 370 m_selection.extend_to(first_glyph); 371 set_active_glyph(first_glyph, ShouldResetSelection::No); 372 scroll_to_glyph(m_active_glyph); 373 return; 374 } 375 auto start_of_row = (m_active_glyph - first_glyph) / m_columns * m_columns; 376 if (event.shift()) 377 m_selection.extend_to(start_of_row + first_glyph); 378 set_active_glyph(start_of_row + first_glyph, event.shift() ? ShouldResetSelection::No : ShouldResetSelection::Yes); 379 return; 380 } 381 382 if (event.key() == KeyCode::Key_End) { 383 if (event.alt()) { 384 set_active_glyph(last_glyph); 385 scroll_to_glyph(m_active_glyph); 386 return; 387 } 388 if (event.ctrl() && event.shift()) { 389 m_selection.extend_to(last_glyph); 390 set_active_glyph(last_glyph, ShouldResetSelection::No); 391 scroll_to_glyph(m_active_glyph); 392 return; 393 } 394 auto end_of_row = (m_active_glyph - first_glyph) / m_columns * m_columns + (m_columns - 1); 395 end_of_row = clamp(end_of_row + first_glyph, first_glyph, last_glyph); 396 if (event.shift()) 397 m_selection.extend_to(end_of_row); 398 set_active_glyph(end_of_row, event.shift() ? ShouldResetSelection::No : ShouldResetSelection::Yes); 399 return; 400 } 401 402 { 403 auto first_visible_row = vertical_scrollbar().value() / vertical_scrollbar().step(); 404 auto last_visible_row = first_visible_row + m_visible_rows; 405 auto current_row = (m_active_glyph - first_glyph) / columns(); 406 auto page = m_active_glyph; 407 408 if (event.key() == KeyCode::Key_PageDown) { 409 auto current_page = m_active_glyph + m_columns * (last_visible_row - current_row); 410 auto next_page = m_active_glyph + m_columns * m_visible_rows; 411 auto remainder = m_active_glyph + m_columns * ((last_glyph - first_glyph) / columns() - current_row); 412 if (current_row < last_visible_row && current_page <= last_glyph) 413 page = current_page; 414 else if (next_page <= last_glyph) 415 page = next_page; 416 else if (remainder <= last_glyph) 417 page = remainder; 418 else 419 page = remainder - m_columns; // Bottom rows do not always extend across all columns 420 if (event.shift()) 421 m_selection.extend_to(page); 422 set_active_glyph(page, event.shift() ? ShouldResetSelection::No : ShouldResetSelection::Yes); 423 scroll_to_glyph(m_active_glyph); 424 return; 425 } 426 427 if (event.key() == KeyCode::Key_PageUp) { 428 auto current_page = m_active_glyph - m_columns * (current_row - first_visible_row); 429 auto previous_page = m_active_glyph - m_columns * m_visible_rows; 430 auto remainder = m_active_glyph - m_columns * current_row; 431 if (current_row > first_visible_row && current_page >= first_glyph) 432 page = current_page; 433 else if (previous_page >= first_glyph) 434 page = previous_page; 435 else 436 page = remainder; 437 if (event.shift()) 438 m_selection.extend_to(page); 439 set_active_glyph(page, event.shift() ? ShouldResetSelection::No : ShouldResetSelection::Yes); 440 scroll_to_glyph(m_active_glyph); 441 return; 442 } 443 } 444 445 event.ignore(); 446} 447 448void GlyphMapWidget::did_change_font() 449{ 450 recalculate_content_size(); 451 vertical_scrollbar().set_step(font().pixel_size_rounded_up() + m_vertical_spacing); 452} 453 454void GlyphMapWidget::scroll_to_glyph(int glyph) 455{ 456 glyph -= m_active_range.first; 457 int row = glyph / columns(); 458 int column = glyph % columns(); 459 auto scroll_rect = Gfx::IntRect { 460 column * (font().max_glyph_width() + m_horizontal_spacing), 461 row * (font().pixel_size_rounded_up() + m_vertical_spacing), 462 font().max_glyph_width() + m_horizontal_spacing, 463 font().pixel_size_rounded_up() + m_vertical_spacing 464 }; 465 scroll_into_view(scroll_rect, true, true); 466} 467 468void GlyphMapWidget::select_previous_existing_glyph() 469{ 470 bool search_wrapped = false; 471 int first_glyph = m_active_range.first; 472 int last_glyph = m_active_range.last; 473 for (int i = active_glyph() - 1;; --i) { 474 if (i < first_glyph && !search_wrapped) { 475 i = last_glyph; 476 search_wrapped = true; 477 } else if (i < first_glyph && search_wrapped) { 478 break; 479 } 480 if (font().contains_glyph(i)) { 481 set_focus(true); 482 set_active_glyph(i); 483 scroll_to_glyph(i); 484 break; 485 } 486 } 487} 488 489void GlyphMapWidget::select_next_existing_glyph() 490{ 491 bool search_wrapped = false; 492 int first_glyph = m_active_range.first; 493 int last_glyph = m_active_range.last; 494 for (int i = active_glyph() + 1;; ++i) { 495 if (i > last_glyph && !search_wrapped) { 496 i = first_glyph; 497 search_wrapped = true; 498 } else if (i > last_glyph && search_wrapped) { 499 break; 500 } 501 if (font().contains_glyph(i)) { 502 set_focus(true); 503 set_active_glyph(i); 504 scroll_to_glyph(i); 505 break; 506 } 507 } 508} 509 510void GlyphMapWidget::recalculate_content_size() 511{ 512 auto event_width = widget_inner_rect().width(); 513 auto event_height = widget_inner_rect().height(); 514 m_columns = max(event_width / (font().max_glyph_width() + m_horizontal_spacing), 1); 515 m_rows = ceil_div(m_glyph_count, m_columns); 516 517 constexpr auto overdraw_margins = 2; 518 auto max_visible_rows = event_height / (font().pixel_size_rounded_up() + m_vertical_spacing); 519 m_visible_rows = min(max_visible_rows, m_rows); 520 m_visible_glyphs = (m_visible_rows + overdraw_margins) * m_columns; 521 522 int content_width = columns() * (font().max_glyph_width() + m_horizontal_spacing); 523 int content_height = rows() * (font().pixel_size_rounded_up() + m_vertical_spacing); 524 set_content_size({ content_width, content_height }); 525 526 scroll_to_glyph(m_active_glyph); 527} 528 529void GlyphMapWidget::set_active_range(Unicode::CodePointRange range) 530{ 531 if (m_active_range.first == range.first && m_active_range.last == range.last) 532 return; 533 m_active_range = range; 534 m_glyph_count = range.last - range.first + 1; 535 set_active_glyph(range.first); 536 vertical_scrollbar().set_value(0); 537 recalculate_content_size(); 538 update(); 539} 540 541void GlyphMapWidget::set_highlight_modifications(bool highlight_modifications) 542{ 543 if (m_highlight_modifications == highlight_modifications) 544 return; 545 546 m_highlight_modifications = highlight_modifications; 547 update(); 548} 549 550void GlyphMapWidget::set_show_system_emoji(bool show) 551{ 552 if (m_show_system_emoji == show) 553 return; 554 m_show_system_emoji = show; 555 update(); 556} 557 558void GlyphMapWidget::set_glyph_modified(u32 glyph, bool modified) 559{ 560 if (modified) 561 m_modified_glyphs.set(glyph); 562 else 563 m_modified_glyphs.remove(glyph); 564} 565 566bool GlyphMapWidget::glyph_is_modified(u32 glyph) 567{ 568 return m_modified_glyphs.contains(glyph); 569} 570 571ErrorOr<void> GlyphMapWidget::set_font(Gfx::Font const& font) 572{ 573 m_original_font = TRY(font.try_clone()); 574 m_modified_glyphs.clear(); 575 AbstractScrollableWidget::set_font(font); 576 return {}; 577} 578 579void GlyphMapWidget::enter_event(Core::Event&) 580{ 581 set_automatic_scrolling_timer_active(false); 582} 583 584void GlyphMapWidget::leave_event(Core::Event&) 585{ 586 if (m_in_drag_select) 587 set_automatic_scrolling_timer_active(true); 588} 589 590Optional<UISize> GlyphMapWidget::calculated_min_size() const 591{ 592 auto scrollbar = vertical_scrollbar().effective_min_size().height().as_int(); 593 auto min_height = max(font().pixel_size_rounded_up() + m_vertical_spacing, scrollbar); 594 auto min_width = font().max_glyph_width() + m_horizontal_spacing + width_occupied_by_vertical_scrollbar(); 595 return { { min_width + frame_thickness() * 2, min_height + frame_thickness() * 2 } }; 596} 597 598}