Serenity Operating System
at hosted 1532 lines 52 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include <AK/QuickSort.h> 28#include <AK/StringBuilder.h> 29#include <Kernel/KeyCode.h> 30#include <LibGUI/Action.h> 31#include <LibGUI/Clipboard.h> 32#include <LibGUI/FontDatabase.h> 33#include <LibGUI/InputBox.h> 34#include <LibGUI/Menu.h> 35#include <LibGUI/Painter.h> 36#include <LibGUI/ScrollBar.h> 37#include <LibGUI/SyntaxHighlighter.h> 38#include <LibGUI/TextEditor.h> 39#include <LibGUI/Window.h> 40#include <LibGfx/Bitmap.h> 41#include <LibGfx/Font.h> 42#include <LibGfx/Palette.h> 43#include <ctype.h> 44#include <fcntl.h> 45#include <stdio.h> 46#include <unistd.h> 47 48//#define DEBUG_TEXTEDITOR 49 50namespace GUI { 51 52TextEditor::TextEditor(Type type) 53 : m_type(type) 54{ 55 set_background_role(ColorRole::Base); 56 set_foreground_role(ColorRole::BaseText); 57 set_document(TextDocument::create()); 58 set_scrollbars_enabled(is_multi_line()); 59 set_font(FontDatabase::the().get_by_name("Csilla Thin")); 60 // FIXME: Recompute vertical scrollbar step size on font change. 61 vertical_scrollbar().set_step(line_height()); 62 m_cursor = { 0, 0 }; 63 create_actions(); 64} 65 66TextEditor::~TextEditor() 67{ 68 if (m_document) 69 m_document->unregister_client(*this); 70} 71 72void TextEditor::create_actions() 73{ 74 m_undo_action = CommonActions::make_undo_action([&](auto&) { undo(); }, this); 75 m_redo_action = CommonActions::make_redo_action([&](auto&) { redo(); }, this); 76 m_undo_action->set_enabled(false); 77 m_redo_action->set_enabled(false); 78 m_cut_action = CommonActions::make_cut_action([&](auto&) { cut(); }, this); 79 m_copy_action = CommonActions::make_copy_action([&](auto&) { copy(); }, this); 80 m_paste_action = CommonActions::make_paste_action([&](auto&) { paste(); }, this); 81 m_delete_action = CommonActions::make_delete_action([&](auto&) { do_delete(); }, this); 82 if (is_multi_line()) { 83 m_go_to_line_action = Action::create( 84 "Go to line...", { Mod_Ctrl, Key_L }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](auto&) { 85 auto input_box = InputBox::construct("Line:", "Go to line", window()); 86 auto result = input_box->exec(); 87 if (result == InputBox::ExecOK) { 88 bool ok; 89 auto line_number = input_box->text_value().to_uint(ok); 90 if (ok) 91 set_cursor(line_number - 1, 0); 92 } 93 }, 94 this); 95 } 96} 97 98void TextEditor::set_text(const StringView& text) 99{ 100 if (is_single_line() && text.length() == line(0).length() && !memcmp(text.characters_without_null_termination(), line(0).characters(), text.length())) 101 return; 102 103 m_selection.clear(); 104 105 document().set_text(text); 106 107 update_content_size(); 108 recompute_all_visual_lines(); 109 if (is_single_line()) 110 set_cursor(0, line(0).length()); 111 else 112 set_cursor(0, 0); 113 did_update_selection(); 114 update(); 115} 116 117void TextEditor::update_content_size() 118{ 119 int content_width = 0; 120 int content_height = 0; 121 for (auto& line : m_line_visual_data) { 122 content_width = max(line.visual_rect.width(), content_width); 123 content_height += line.visual_rect.height(); 124 } 125 content_width += m_horizontal_content_padding * 2; 126 if (is_right_text_alignment(m_text_alignment)) 127 content_width = max(frame_inner_rect().width(), content_width); 128 129 set_content_size({ content_width, content_height }); 130 set_size_occupied_by_fixed_elements({ ruler_width(), 0 }); 131} 132 133TextPosition TextEditor::text_position_at(const Gfx::Point& a_position) const 134{ 135 auto position = a_position; 136 position.move_by(horizontal_scrollbar().value(), vertical_scrollbar().value()); 137 position.move_by(-(m_horizontal_content_padding + ruler_width()), 0); 138 position.move_by(-frame_thickness(), -frame_thickness()); 139 140 size_t line_index = 0; 141 142 if (is_line_wrapping_enabled()) { 143 for (size_t i = 0; i < line_count(); ++i) { 144 auto& rect = m_line_visual_data[i].visual_rect; 145 if (position.y() >= rect.top() && position.y() <= rect.bottom()) { 146 line_index = i; 147 break; 148 } 149 if (position.y() > rect.bottom()) 150 line_index = line_count() - 1; 151 } 152 } else { 153 line_index = (size_t)(position.y() / line_height()); 154 } 155 156 line_index = max((size_t)0, min(line_index, line_count() - 1)); 157 158 size_t column_index; 159 switch (m_text_alignment) { 160 case Gfx::TextAlignment::CenterLeft: 161 column_index = (position.x() + glyph_width() / 2) / glyph_width(); 162 if (is_line_wrapping_enabled()) { 163 for_each_visual_line(line_index, [&](const Gfx::Rect& rect, const StringView&, size_t start_of_line) { 164 if (rect.contains_vertically(position.y())) { 165 column_index += start_of_line; 166 return IterationDecision::Break; 167 } 168 return IterationDecision::Continue; 169 }); 170 } 171 break; 172 case Gfx::TextAlignment::CenterRight: 173 // FIXME: Support right-aligned line wrapping, I guess. 174 ASSERT(!is_line_wrapping_enabled()); 175 column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width(); 176 break; 177 default: 178 ASSERT_NOT_REACHED(); 179 } 180 181 column_index = max((size_t)0, min(column_index, line(line_index).length())); 182 return { line_index, column_index }; 183} 184 185void TextEditor::doubleclick_event(MouseEvent& event) 186{ 187 if (event.button() != MouseButton::Left) 188 return; 189 190 // NOTE: This ensures that spans are updated before we look at them. 191 flush_pending_change_notification_if_needed(); 192 193 m_triple_click_timer.start(); 194 m_in_drag_select = false; 195 196 auto start = text_position_at(event.position()); 197 auto end = start; 198 auto& line = this->line(start.line()); 199 200 if (!document().has_spans()) { 201 while (start.column() > 0) { 202 if (isspace(line.characters()[start.column() - 1])) 203 break; 204 start.set_column(start.column() - 1); 205 } 206 207 while (end.column() < line.length()) { 208 if (isspace(line.characters()[end.column()])) 209 break; 210 end.set_column(end.column() + 1); 211 } 212 } else { 213 for (auto& span : document().spans()) { 214 if (!span.range.contains(start)) 215 continue; 216 start = span.range.start(); 217 end = span.range.end(); 218 end.set_column(end.column() + 1); 219 break; 220 } 221 } 222 223 m_selection.set(start, end); 224 set_cursor(end); 225 update(); 226 did_update_selection(); 227} 228 229void TextEditor::mousedown_event(MouseEvent& event) 230{ 231 if (event.button() != MouseButton::Left) { 232 return; 233 } 234 235 if (m_triple_click_timer.is_valid() && m_triple_click_timer.elapsed() < 250) { 236 m_triple_click_timer = Core::ElapsedTimer(); 237 238 TextPosition start; 239 TextPosition end; 240 241 if (is_multi_line()) { 242 // select *current* line 243 start = TextPosition(m_cursor.line(), 0); 244 end = TextPosition(m_cursor.line(), line(m_cursor.line()).length()); 245 } else { 246 // select *whole* line 247 start = TextPosition(0, 0); 248 end = TextPosition(line_count() - 1, line(line_count() - 1).length()); 249 } 250 251 m_selection.set(start, end); 252 set_cursor(end); 253 return; 254 } 255 256 if (event.modifiers() & Mod_Shift) { 257 if (!has_selection()) 258 m_selection.set(m_cursor, {}); 259 } else { 260 m_selection.clear(); 261 } 262 263 m_in_drag_select = true; 264 265 set_cursor(text_position_at(event.position())); 266 267 if (!(event.modifiers() & Mod_Shift)) { 268 if (!has_selection()) 269 m_selection.set(m_cursor, {}); 270 } 271 272 if (m_selection.start().is_valid() && m_selection.start() != m_cursor) 273 m_selection.set_end(m_cursor); 274 275 // FIXME: Only update the relevant rects. 276 update(); 277 did_update_selection(); 278} 279 280void TextEditor::mouseup_event(MouseEvent& event) 281{ 282 if (event.button() == MouseButton::Left) { 283 if (m_in_drag_select) { 284 m_in_drag_select = false; 285 } 286 return; 287 } 288} 289 290void TextEditor::mousemove_event(MouseEvent& event) 291{ 292 if (m_in_drag_select) { 293 set_cursor(text_position_at(event.position())); 294 m_selection.set_end(m_cursor); 295 did_update_selection(); 296 update(); 297 return; 298 } 299} 300 301int TextEditor::ruler_width() const 302{ 303 if (!m_ruler_visible) 304 return 0; 305 // FIXME: Resize based on needed space. 306 return 5 * font().glyph_width('x') + 4; 307} 308 309Gfx::Rect TextEditor::ruler_content_rect(size_t line_index) const 310{ 311 if (!m_ruler_visible) 312 return {}; 313 return { 314 0 - ruler_width() + horizontal_scrollbar().value(), 315 line_content_rect(line_index).y(), 316 ruler_width(), 317 line_content_rect(line_index).height() 318 }; 319} 320 321Gfx::Rect TextEditor::ruler_rect_in_inner_coordinates() const 322{ 323 return { 0, 0, ruler_width(), height() - height_occupied_by_horizontal_scrollbar() }; 324} 325 326Gfx::Rect TextEditor::visible_text_rect_in_inner_coordinates() const 327{ 328 return { 329 m_horizontal_content_padding + (m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + 1) : 0), 330 0, 331 frame_inner_rect().width() - (m_horizontal_content_padding * 2) - width_occupied_by_vertical_scrollbar() - ruler_width(), 332 frame_inner_rect().height() - height_occupied_by_horizontal_scrollbar() 333 }; 334} 335 336void TextEditor::paint_event(PaintEvent& event) 337{ 338 Color widget_background_color = palette().color(background_role()); 339 // NOTE: This ensures that spans are updated before we look at them. 340 flush_pending_change_notification_if_needed(); 341 342 Frame::paint_event(event); 343 344 Painter painter(*this); 345 painter.add_clip_rect(widget_inner_rect()); 346 painter.add_clip_rect(event.rect()); 347 painter.fill_rect(event.rect(), widget_background_color); 348 349 painter.translate(frame_thickness(), frame_thickness()); 350 351 auto ruler_rect = ruler_rect_in_inner_coordinates(); 352 353 if (m_ruler_visible) { 354 painter.fill_rect(ruler_rect, palette().ruler()); 355 painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), palette().ruler_border()); 356 } 357 358 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); 359 if (m_ruler_visible) 360 painter.translate(ruler_width(), 0); 361 362 size_t first_visible_line = text_position_at(event.rect().top_left()).line(); 363 size_t last_visible_line = text_position_at(event.rect().bottom_right()).line(); 364 365 auto selection = normalized_selection(); 366 bool has_selection = selection.is_valid(); 367 368 if (m_ruler_visible) { 369 for (size_t i = first_visible_line; i <= last_visible_line; ++i) { 370 bool is_current_line = i == m_cursor.line(); 371 auto ruler_line_rect = ruler_content_rect(i); 372 painter.draw_text( 373 ruler_line_rect.shrunken(2, 0).translated(0, m_line_spacing / 2), 374 String::number(i + 1), 375 is_current_line ? Gfx::Font::default_bold_font() : font(), 376 Gfx::TextAlignment::TopRight, 377 is_current_line ? palette().ruler_active_text() : palette().ruler_inactive_text()); 378 } 379 } 380 381 Gfx::Rect text_clip_rect { 382 (m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + frame_thickness() + 1) : frame_thickness()), 383 frame_thickness(), 384 width() - width_occupied_by_vertical_scrollbar() - ruler_width(), 385 height() - height_occupied_by_horizontal_scrollbar() 386 }; 387 painter.add_clip_rect(text_clip_rect); 388 389 for (size_t line_index = first_visible_line; line_index <= last_visible_line; ++line_index) { 390 auto& line = this->line(line_index); 391 392 bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line(); 393 size_t first_visual_line_with_selection = 0; 394 size_t last_visual_line_with_selection = 0; 395 if (physical_line_has_selection) { 396 if (selection.start().line() < line_index) 397 first_visual_line_with_selection = 0; 398 else 399 first_visual_line_with_selection = visual_line_containing(line_index, selection.start().column()); 400 401 if (selection.end().line() > line_index) 402 last_visual_line_with_selection = m_line_visual_data[line_index].visual_line_breaks.size(); 403 else 404 last_visual_line_with_selection = visual_line_containing(line_index, selection.end().column()); 405 } 406 407 size_t selection_start_column_within_line = selection.start().line() == line_index ? selection.start().column() : 0; 408 size_t selection_end_column_within_line = selection.end().line() == line_index ? selection.end().column() : line.length(); 409 410 size_t visual_line_index = 0; 411 for_each_visual_line(line_index, [&](const Gfx::Rect& visual_line_rect, const StringView& visual_line_text, size_t start_of_visual_line) { 412 if (is_multi_line() && line_index == m_cursor.line()) 413 painter.fill_rect(visual_line_rect, widget_background_color.darkened(0.9f)); 414#ifdef DEBUG_TEXTEDITOR 415 painter.draw_rect(visual_line_rect, Color::Cyan); 416#endif 417 if (!document().has_spans()) { 418 // Fast-path for plain text 419 auto color = palette().color(is_enabled() ? foreground_role() : Gfx::ColorRole::DisabledText); 420 painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, color); 421 } else { 422 int advance = font().glyph_width(' ') + font().glyph_spacing(); 423 Gfx::Rect character_rect = { visual_line_rect.location(), { font().glyph_width(' '), line_height() } }; 424 for (size_t i = 0; i < visual_line_text.length(); ++i) { 425 const Gfx::Font* font = &this->font(); 426 Color color; 427 Optional<Color> background_color; 428 bool underline = false; 429 TextPosition physical_position(line_index, start_of_visual_line + i); 430 // FIXME: This is *horribly* inefficient. 431 for (auto& span : document().spans()) { 432 if (!span.range.contains(physical_position)) 433 continue; 434 color = span.color; 435 if (span.font) 436 font = span.font; 437 background_color = span.background_color; 438 underline = span.is_underlined; 439 break; 440 } 441 if (background_color.has_value()) 442 painter.fill_rect(character_rect, background_color.value()); 443 painter.draw_text(character_rect, visual_line_text.substring_view(i, 1), *font, m_text_alignment, color); 444 if (underline) { 445 painter.draw_line(character_rect.bottom_left().translated(0, 1), character_rect.bottom_right().translated(0, 1), color); 446 } 447 character_rect.move_by(advance, 0); 448 } 449 } 450 bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line(); 451 if (physical_line_has_selection) { 452 453 bool current_visual_line_has_selection = (line_index != selection.start().line() && line_index != selection.end().line()) 454 || (visual_line_index >= first_visual_line_with_selection && visual_line_index <= last_visual_line_with_selection); 455 if (current_visual_line_has_selection) { 456 bool selection_begins_on_current_visual_line = visual_line_index == first_visual_line_with_selection; 457 bool selection_ends_on_current_visual_line = visual_line_index == last_visual_line_with_selection; 458 459 int selection_left = selection_begins_on_current_visual_line 460 ? content_x_for_position({ line_index, (size_t)selection_start_column_within_line }) 461 : m_horizontal_content_padding; 462 463 int selection_right = selection_ends_on_current_visual_line 464 ? content_x_for_position({ line_index, (size_t)selection_end_column_within_line }) 465 : visual_line_rect.right() + 1; 466 467 Gfx::Rect selection_rect { 468 selection_left, 469 visual_line_rect.y(), 470 selection_right - selection_left, 471 visual_line_rect.height() 472 }; 473 474 Color background_color = is_focused() ? palette().selection() : palette().inactive_selection(); 475 Color text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text(); 476 477 painter.fill_rect(selection_rect, background_color); 478 479 size_t start_of_selection_within_visual_line = (size_t)max(0, (int)selection_start_column_within_line - (int)start_of_visual_line); 480 size_t end_of_selection_within_visual_line = selection_end_column_within_line - start_of_visual_line; 481 482 StringView visual_selected_text { 483 visual_line_text.characters_without_null_termination() + start_of_selection_within_visual_line, 484 end_of_selection_within_visual_line - start_of_selection_within_visual_line 485 }; 486 487 painter.draw_text(selection_rect, visual_selected_text, Gfx::TextAlignment::CenterLeft, text_color); 488 } 489 } 490 ++visual_line_index; 491 return IterationDecision::Continue; 492 }); 493 } 494 495 if (is_focused() && m_cursor_state) 496 painter.fill_rect(cursor_content_rect(), palette().text_cursor()); 497} 498 499void TextEditor::toggle_selection_if_needed_for_event(const KeyEvent& event) 500{ 501 if (event.shift() && !m_selection.is_valid()) { 502 m_selection.set(m_cursor, {}); 503 did_update_selection(); 504 update(); 505 return; 506 } 507 if (!event.shift() && m_selection.is_valid()) { 508 m_selection.clear(); 509 did_update_selection(); 510 update(); 511 return; 512 } 513} 514 515void TextEditor::select_all() 516{ 517 TextPosition start_of_document { 0, 0 }; 518 TextPosition end_of_document { line_count() - 1, line(line_count() - 1).length() }; 519 m_selection.set(start_of_document, end_of_document); 520 did_update_selection(); 521 set_cursor(end_of_document); 522 update(); 523} 524 525void TextEditor::get_selection_line_boundaries(size_t& first_line, size_t& last_line) 526{ 527 auto selection = normalized_selection(); 528 if (!selection.is_valid()) { 529 first_line = m_cursor.line(); 530 last_line = m_cursor.line(); 531 return; 532 } 533 first_line = selection.start().line(); 534 last_line = selection.end().line(); 535 if (first_line != last_line && selection.end().column() == 0) 536 last_line -= 1; 537} 538 539void TextEditor::move_selected_lines_up() 540{ 541 size_t first_line; 542 size_t last_line; 543 get_selection_line_boundaries(first_line, last_line); 544 545 if (first_line == 0) 546 return; 547 548 auto& lines = document().lines(); 549 lines.insert((int)last_line, lines.take((int)first_line - 1)); 550 m_cursor = { first_line - 1, 0 }; 551 552 if (has_selection()) { 553 m_selection.set_start({ first_line - 1, 0 }); 554 m_selection.set_end({ last_line - 1, line(last_line - 1).length() }); 555 } 556 557 did_change(); 558 update(); 559} 560 561void TextEditor::move_selected_lines_down() 562{ 563 size_t first_line; 564 size_t last_line; 565 get_selection_line_boundaries(first_line, last_line); 566 567 auto& lines = document().lines(); 568 ASSERT(lines.size() != 0); 569 if (last_line >= lines.size() - 1) 570 return; 571 572 lines.insert((int)first_line, lines.take((int)last_line + 1)); 573 m_cursor = { first_line + 1, 0 }; 574 575 if (has_selection()) { 576 m_selection.set_start({ first_line + 1, 0 }); 577 m_selection.set_end({ last_line + 1, line(last_line + 1).length() }); 578 } 579 580 did_change(); 581 update(); 582} 583 584void TextEditor::sort_selected_lines() 585{ 586 if (is_readonly()) 587 return; 588 589 if (!has_selection()) 590 return; 591 592 size_t first_line; 593 size_t last_line; 594 get_selection_line_boundaries(first_line, last_line); 595 596 auto& lines = document().lines(); 597 598 auto start = lines.begin() + (int)first_line; 599 auto end = lines.begin() + (int)last_line + 1; 600 601 quick_sort(start, end, [](auto& a, auto& b) { 602 return strcmp(a.characters(), b.characters()) < 0; 603 }); 604 605 did_change(); 606 update(); 607} 608 609void TextEditor::keydown_event(KeyEvent& event) 610{ 611 if (is_single_line() && event.key() == KeyCode::Key_Tab) 612 return Widget::keydown_event(event); 613 614 if (is_single_line() && event.key() == KeyCode::Key_Return) { 615 if (on_return_pressed) 616 on_return_pressed(); 617 return; 618 } 619 620 if (event.key() == KeyCode::Key_Escape) { 621 if (on_escape_pressed) 622 on_escape_pressed(); 623 return; 624 } 625 if (is_multi_line() && event.key() == KeyCode::Key_Up) { 626 if (m_cursor.line() > 0) { 627 if (event.ctrl() && event.shift()) { 628 move_selected_lines_up(); 629 return; 630 } 631 size_t new_line = m_cursor.line() - 1; 632 size_t new_column = min(m_cursor.column(), line(new_line).length()); 633 toggle_selection_if_needed_for_event(event); 634 set_cursor(new_line, new_column); 635 if (event.shift() && m_selection.start().is_valid()) { 636 m_selection.set_end(m_cursor); 637 did_update_selection(); 638 } 639 } 640 return; 641 } 642 if (is_multi_line() && event.key() == KeyCode::Key_Down) { 643 if (m_cursor.line() < (line_count() - 1)) { 644 if (event.ctrl() && event.shift()) { 645 move_selected_lines_down(); 646 return; 647 } 648 size_t new_line = m_cursor.line() + 1; 649 size_t new_column = min(m_cursor.column(), line(new_line).length()); 650 toggle_selection_if_needed_for_event(event); 651 set_cursor(new_line, new_column); 652 if (event.shift() && m_selection.start().is_valid()) { 653 m_selection.set_end(m_cursor); 654 did_update_selection(); 655 } 656 } 657 return; 658 } 659 if (is_multi_line() && event.key() == KeyCode::Key_PageUp) { 660 if (m_cursor.line() > 0) { 661 size_t page_step = (size_t)visible_content_rect().height() / (size_t)line_height(); 662 size_t new_line = m_cursor.line() < page_step ? 0 : m_cursor.line() - page_step; 663 size_t new_column = min(m_cursor.column(), line(new_line).length()); 664 toggle_selection_if_needed_for_event(event); 665 set_cursor(new_line, new_column); 666 if (event.shift() && m_selection.start().is_valid()) { 667 m_selection.set_end(m_cursor); 668 did_update_selection(); 669 } 670 } 671 return; 672 } 673 if (is_multi_line() && event.key() == KeyCode::Key_PageDown) { 674 if (m_cursor.line() < (line_count() - 1)) { 675 int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height()); 676 int new_column = min(m_cursor.column(), lines()[new_line].length()); 677 toggle_selection_if_needed_for_event(event); 678 set_cursor(new_line, new_column); 679 if (event.shift() && m_selection.start().is_valid()) { 680 m_selection.set_end(m_cursor); 681 did_update_selection(); 682 } 683 } 684 return; 685 } 686 if (event.key() == KeyCode::Key_Left) { 687 if (event.ctrl() && document().has_spans()) { 688 // FIXME: Do something nice when the document has no spans. 689 auto span = document().first_non_skippable_span_before(m_cursor); 690 TextPosition new_cursor = !span.has_value() 691 ? TextPosition(0, 0) 692 : span.value().range.start(); 693 toggle_selection_if_needed_for_event(event); 694 set_cursor(new_cursor); 695 if (event.shift() && m_selection.start().is_valid()) { 696 m_selection.set_end(m_cursor); 697 did_update_selection(); 698 } 699 return; 700 } 701 if (m_cursor.column() > 0) { 702 int new_column = m_cursor.column() - 1; 703 toggle_selection_if_needed_for_event(event); 704 set_cursor(m_cursor.line(), new_column); 705 if (event.shift() && m_selection.start().is_valid()) { 706 m_selection.set_end(m_cursor); 707 did_update_selection(); 708 } 709 } else if (m_cursor.line() > 0) { 710 int new_line = m_cursor.line() - 1; 711 int new_column = lines()[new_line].length(); 712 toggle_selection_if_needed_for_event(event); 713 set_cursor(new_line, new_column); 714 if (event.shift() && m_selection.start().is_valid()) { 715 m_selection.set_end(m_cursor); 716 did_update_selection(); 717 } 718 } 719 return; 720 } 721 if (event.key() == KeyCode::Key_Right) { 722 if (event.ctrl() && document().has_spans()) { 723 // FIXME: Do something nice when the document has no spans. 724 auto span = document().first_non_skippable_span_after(m_cursor); 725 TextPosition new_cursor = !span.has_value() 726 ? document().spans().last().range.end() 727 : span.value().range.start(); 728 toggle_selection_if_needed_for_event(event); 729 set_cursor(new_cursor); 730 if (event.shift() && m_selection.start().is_valid()) { 731 m_selection.set_end(m_cursor); 732 did_update_selection(); 733 } 734 return; 735 } 736 int new_line = m_cursor.line(); 737 int new_column = m_cursor.column(); 738 if (m_cursor.column() < current_line().length()) { 739 new_line = m_cursor.line(); 740 new_column = m_cursor.column() + 1; 741 } else if (m_cursor.line() != line_count() - 1) { 742 new_line = m_cursor.line() + 1; 743 new_column = 0; 744 } 745 toggle_selection_if_needed_for_event(event); 746 set_cursor(new_line, new_column); 747 if (event.shift() && m_selection.start().is_valid()) { 748 m_selection.set_end(m_cursor); 749 did_update_selection(); 750 } 751 return; 752 } 753 if (!event.ctrl() && event.key() == KeyCode::Key_Home) { 754 size_t first_nonspace_column = current_line().first_non_whitespace_column(); 755 toggle_selection_if_needed_for_event(event); 756 if (m_cursor.column() == first_nonspace_column) 757 set_cursor(m_cursor.line(), 0); 758 else 759 set_cursor(m_cursor.line(), first_nonspace_column); 760 if (event.shift() && m_selection.start().is_valid()) { 761 m_selection.set_end(m_cursor); 762 did_update_selection(); 763 } 764 return; 765 } 766 if (!event.ctrl() && event.key() == KeyCode::Key_End) { 767 toggle_selection_if_needed_for_event(event); 768 set_cursor(m_cursor.line(), current_line().length()); 769 if (event.shift() && m_selection.start().is_valid()) { 770 m_selection.set_end(m_cursor); 771 did_update_selection(); 772 } 773 return; 774 } 775 if (event.ctrl() && event.key() == KeyCode::Key_Home) { 776 toggle_selection_if_needed_for_event(event); 777 set_cursor(0, 0); 778 if (event.shift() && m_selection.start().is_valid()) { 779 m_selection.set_end(m_cursor); 780 did_update_selection(); 781 } 782 return; 783 } 784 if (event.ctrl() && event.key() == KeyCode::Key_End) { 785 toggle_selection_if_needed_for_event(event); 786 set_cursor(line_count() - 1, lines()[line_count() - 1].length()); 787 if (event.shift() && m_selection.start().is_valid()) { 788 m_selection.set_end(m_cursor); 789 did_update_selection(); 790 } 791 return; 792 } 793 if (event.modifiers() == Mod_Ctrl && event.key() == KeyCode::Key_A) { 794 select_all(); 795 return; 796 } 797 if (event.alt() && event.shift() && event.key() == KeyCode::Key_S) { 798 sort_selected_lines(); 799 return; 800 } 801 if (event.key() == KeyCode::Key_Backspace) { 802 if (is_readonly()) 803 return; 804 if (has_selection()) { 805 delete_selection(); 806 did_update_selection(); 807 return; 808 } 809 if (m_cursor.column() > 0) { 810 int erase_count = 1; 811 if (current_line().first_non_whitespace_column() >= m_cursor.column()) { 812 int new_column; 813 if (m_cursor.column() % m_soft_tab_width == 0) 814 new_column = m_cursor.column() - m_soft_tab_width; 815 else 816 new_column = (m_cursor.column() / m_soft_tab_width) * m_soft_tab_width; 817 erase_count = m_cursor.column() - new_column; 818 } 819 820 // Backspace within line 821 TextRange erased_range({ m_cursor.line(), m_cursor.column() - erase_count }, m_cursor); 822 auto erased_text = document().text_in_range(erased_range); 823 execute<RemoveTextCommand>(erased_text, erased_range); 824 return; 825 } 826 if (m_cursor.column() == 0 && m_cursor.line() != 0) { 827 // Backspace at column 0; merge with previous line 828 size_t previous_length = line(m_cursor.line() - 1).length(); 829 TextRange erased_range({ m_cursor.line() - 1, previous_length }, m_cursor); 830 execute<RemoveTextCommand>("\n", erased_range); 831 return; 832 } 833 return; 834 } 835 836 if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) { 837 if (is_readonly()) 838 return; 839 delete_current_line(); 840 return; 841 } 842 843 if (event.key() == KeyCode::Key_Delete) { 844 if (is_readonly()) 845 return; 846 do_delete(); 847 return; 848 } 849 850 if (!is_readonly() && !event.ctrl() && !event.alt() && !event.text().is_empty()) { 851 insert_at_cursor_or_replace_selection(event.text()); 852 return; 853 } 854 855 event.ignore(); 856} 857 858void TextEditor::delete_current_line() 859{ 860 if (has_selection()) 861 return delete_selection(); 862 863 TextPosition start; 864 TextPosition end; 865 if (m_cursor.line() == 0 && line_count() == 1) { 866 start = { 0, 0 }; 867 end = { 0, line(0).length() }; 868 } else if (m_cursor.line() == line_count() - 1) { 869 start = { m_cursor.line() - 1, line(m_cursor.line()).length() }; 870 end = { m_cursor.line(), line(m_cursor.line()).length() }; 871 } else { 872 start = { m_cursor.line(), 0 }; 873 end = { m_cursor.line() + 1, 0 }; 874 } 875 876 TextRange erased_range(start, end); 877 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range); 878} 879 880void TextEditor::do_delete() 881{ 882 if (is_readonly()) 883 return; 884 885 if (has_selection()) 886 return delete_selection(); 887 888 if (m_cursor.column() < current_line().length()) { 889 // Delete within line 890 TextRange erased_range(m_cursor, { m_cursor.line(), m_cursor.column() + 1 }); 891 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range); 892 return; 893 } 894 if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) { 895 // Delete at end of line; merge with next line 896 TextRange erased_range(m_cursor, { m_cursor.line() + 1, 0 }); 897 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range); 898 return; 899 } 900} 901 902int TextEditor::content_x_for_position(const TextPosition& position) const 903{ 904 auto& line = this->line(position.line()); 905 int x_offset = -1; 906 switch (m_text_alignment) { 907 case Gfx::TextAlignment::CenterLeft: 908 for_each_visual_line(position.line(), [&](const Gfx::Rect&, const StringView& view, size_t start_of_visual_line) { 909 if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) { 910 x_offset = (position.column() - start_of_visual_line) * glyph_width(); 911 return IterationDecision::Break; 912 } 913 return IterationDecision::Continue; 914 }); 915 return m_horizontal_content_padding + x_offset; 916 case Gfx::TextAlignment::CenterRight: 917 // FIXME 918 ASSERT(!is_line_wrapping_enabled()); 919 return content_width() - m_horizontal_content_padding - (line.length() * glyph_width()) + (position.column() * glyph_width()); 920 default: 921 ASSERT_NOT_REACHED(); 922 } 923} 924 925Gfx::Rect TextEditor::content_rect_for_position(const TextPosition& position) const 926{ 927 if (!position.is_valid()) 928 return {}; 929 ASSERT(!lines().is_empty()); 930 ASSERT(position.column() <= (current_line().length() + 1)); 931 932 int x = content_x_for_position(position); 933 934 if (is_single_line()) { 935 Gfx::Rect rect { x, 0, 1, font().glyph_height() + 2 }; 936 rect.center_vertically_within({ {}, frame_inner_rect().size() }); 937 return rect; 938 } 939 940 Gfx::Rect rect; 941 for_each_visual_line(position.line(), [&](const Gfx::Rect& visual_line_rect, const StringView& view, size_t start_of_visual_line) { 942 if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) { 943 // NOTE: We have to subtract the horizontal padding here since it's part of the visual line rect 944 // *and* included in what we get from content_x_for_position(). 945 rect = { 946 visual_line_rect.x() + x - (m_horizontal_content_padding), 947 visual_line_rect.y(), 948 1, 949 line_height() 950 }; 951 return IterationDecision::Break; 952 } 953 return IterationDecision::Continue; 954 }); 955 return rect; 956} 957 958Gfx::Rect TextEditor::cursor_content_rect() const 959{ 960 return content_rect_for_position(m_cursor); 961} 962 963Gfx::Rect TextEditor::line_widget_rect(size_t line_index) const 964{ 965 auto rect = line_content_rect(line_index); 966 rect.set_x(frame_thickness()); 967 rect.set_width(frame_inner_rect().width()); 968 rect.move_by(0, -(vertical_scrollbar().value())); 969 rect.move_by(0, frame_thickness()); 970 rect.intersect(frame_inner_rect()); 971 return rect; 972} 973 974void TextEditor::scroll_position_into_view(const TextPosition& position) 975{ 976 auto rect = content_rect_for_position(position); 977 if (position.column() == 0) 978 rect.set_x(content_x_for_position({ position.line(), 0 }) - 2); 979 else if (position.column() == line(position.line()).length()) 980 rect.set_x(content_x_for_position({ position.line(), line(position.line()).length() }) + 2); 981 scroll_into_view(rect, true, true); 982} 983 984void TextEditor::scroll_cursor_into_view() 985{ 986 scroll_position_into_view(m_cursor); 987} 988 989Gfx::Rect TextEditor::line_content_rect(size_t line_index) const 990{ 991 auto& line = this->line(line_index); 992 if (is_single_line()) { 993 Gfx::Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, (int)line.length() * glyph_width(), font().glyph_height() + 2 }; 994 line_rect.center_vertically_within({ {}, frame_inner_rect().size() }); 995 return line_rect; 996 } 997 if (is_line_wrapping_enabled()) 998 return m_line_visual_data[line_index].visual_rect; 999 return { 1000 content_x_for_position({ line_index, 0 }), 1001 (int)line_index * line_height(), 1002 (int)line.length() * glyph_width(), 1003 line_height() 1004 }; 1005} 1006 1007void TextEditor::update_cursor() 1008{ 1009 update(line_widget_rect(m_cursor.line())); 1010} 1011 1012void TextEditor::set_cursor(size_t line, size_t column) 1013{ 1014 set_cursor({ line, column }); 1015} 1016 1017void TextEditor::set_cursor(const TextPosition& a_position) 1018{ 1019 ASSERT(!lines().is_empty()); 1020 1021 TextPosition position = a_position; 1022 1023 if (position.line() >= line_count()) 1024 position.set_line(line_count() - 1); 1025 1026 if (position.column() > lines()[position.line()].length()) 1027 position.set_column(lines()[position.line()].length()); 1028 1029 if (m_cursor != position) { 1030 // NOTE: If the old cursor is no longer valid, repaint everything just in case. 1031 auto old_cursor_line_rect = m_cursor.line() < line_count() 1032 ? line_widget_rect(m_cursor.line()) 1033 : rect(); 1034 m_cursor = position; 1035 m_cursor_state = true; 1036 scroll_cursor_into_view(); 1037 update(old_cursor_line_rect); 1038 update_cursor(); 1039 } 1040 cursor_did_change(); 1041 if (on_cursor_change) 1042 on_cursor_change(); 1043 if (m_highlighter) 1044 m_highlighter->cursor_did_change(); 1045} 1046 1047void TextEditor::focusin_event(Core::Event&) 1048{ 1049 update_cursor(); 1050 start_timer(500); 1051} 1052 1053void TextEditor::focusout_event(Core::Event&) 1054{ 1055 stop_timer(); 1056} 1057 1058void TextEditor::timer_event(Core::TimerEvent&) 1059{ 1060 m_cursor_state = !m_cursor_state; 1061 if (is_focused()) 1062 update_cursor(); 1063} 1064 1065bool TextEditor::write_to_file(const StringView& path) 1066{ 1067 int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666); 1068 if (fd < 0) { 1069 perror("open"); 1070 return false; 1071 } 1072 1073 // Compute the final file size and ftruncate() to make writing fast. 1074 // FIXME: Remove this once the kernel is smart enough to do this instead. 1075 off_t file_size = 0; 1076 for (size_t i = 0; i < line_count(); ++i) 1077 file_size += line(i).length(); 1078 file_size += line_count() - 1; 1079 1080 int rc = ftruncate(fd, file_size); 1081 if (rc < 0) { 1082 perror("ftruncate"); 1083 return false; 1084 } 1085 1086 for (size_t i = 0; i < line_count(); ++i) { 1087 auto& line = this->line(i); 1088 if (line.length()) { 1089 ssize_t nwritten = write(fd, line.characters(), line.length()); 1090 if (nwritten < 0) { 1091 perror("write"); 1092 close(fd); 1093 return false; 1094 } 1095 } 1096 if (i != line_count() - 1) { 1097 char ch = '\n'; 1098 ssize_t nwritten = write(fd, &ch, 1); 1099 if (nwritten != 1) { 1100 perror("write"); 1101 close(fd); 1102 return false; 1103 } 1104 } 1105 } 1106 1107 close(fd); 1108 return true; 1109} 1110 1111String TextEditor::text() const 1112{ 1113 StringBuilder builder; 1114 for (size_t i = 0; i < line_count(); ++i) { 1115 auto& line = this->line(i); 1116 builder.append(line.characters(), line.length()); 1117 if (i != line_count() - 1) 1118 builder.append('\n'); 1119 } 1120 return builder.to_string(); 1121} 1122 1123void TextEditor::clear() 1124{ 1125 document().remove_all_lines(); 1126 document().append_line(make<TextDocumentLine>(document())); 1127 m_selection.clear(); 1128 did_update_selection(); 1129 set_cursor(0, 0); 1130 update(); 1131} 1132 1133String TextEditor::selected_text() const 1134{ 1135 if (!has_selection()) 1136 return {}; 1137 1138 return document().text_in_range(m_selection); 1139} 1140 1141void TextEditor::delete_selection() 1142{ 1143 auto selection = normalized_selection(); 1144 execute<RemoveTextCommand>(selected_text(), selection); 1145 m_selection.clear(); 1146 did_update_selection(); 1147 did_change(); 1148 set_cursor(selection.start()); 1149 update(); 1150} 1151 1152void TextEditor::insert_at_cursor_or_replace_selection(const StringView& text) 1153{ 1154 ASSERT(!is_readonly()); 1155 if (has_selection()) 1156 delete_selection(); 1157 execute<InsertTextCommand>(text, m_cursor); 1158} 1159 1160void TextEditor::cut() 1161{ 1162 if (is_readonly()) 1163 return; 1164 auto selected_text = this->selected_text(); 1165 printf("Cut: \"%s\"\n", selected_text.characters()); 1166 Clipboard::the().set_data(selected_text); 1167 delete_selection(); 1168} 1169 1170void TextEditor::copy() 1171{ 1172 auto selected_text = this->selected_text(); 1173 printf("Copy: \"%s\"\n", selected_text.characters()); 1174 Clipboard::the().set_data(selected_text); 1175} 1176 1177void TextEditor::paste() 1178{ 1179 if (is_readonly()) 1180 return; 1181 auto paste_text = Clipboard::the().data(); 1182 printf("Paste: \"%s\"\n", paste_text.characters()); 1183 1184 TemporaryChange change(m_automatic_indentation_enabled, false); 1185 insert_at_cursor_or_replace_selection(paste_text); 1186} 1187 1188void TextEditor::enter_event(Core::Event&) 1189{ 1190 ASSERT(window()); 1191 window()->set_override_cursor(StandardCursor::IBeam); 1192} 1193 1194void TextEditor::leave_event(Core::Event&) 1195{ 1196 ASSERT(window()); 1197 window()->set_override_cursor(StandardCursor::None); 1198} 1199 1200void TextEditor::did_change() 1201{ 1202 update_content_size(); 1203 recompute_all_visual_lines(); 1204 m_undo_action->set_enabled(can_undo()); 1205 m_redo_action->set_enabled(can_redo()); 1206 if (!m_has_pending_change_notification) { 1207 m_has_pending_change_notification = true; 1208 deferred_invoke([this](auto&) { 1209 if (!m_has_pending_change_notification) 1210 return; 1211 if (on_change) 1212 on_change(); 1213 if (m_highlighter) 1214 m_highlighter->rehighlight(palette()); 1215 m_has_pending_change_notification = false; 1216 }); 1217 } 1218} 1219 1220void TextEditor::set_readonly(bool readonly) 1221{ 1222 if (m_readonly == readonly) 1223 return; 1224 m_readonly = readonly; 1225 m_cut_action->set_enabled(!is_readonly() && has_selection()); 1226 m_delete_action->set_enabled(!is_readonly()); 1227 m_paste_action->set_enabled(!is_readonly()); 1228} 1229 1230void TextEditor::did_update_selection() 1231{ 1232 m_cut_action->set_enabled(!is_readonly() && has_selection()); 1233 m_copy_action->set_enabled(has_selection()); 1234 if (on_selection_change) 1235 on_selection_change(); 1236 if (is_line_wrapping_enabled()) { 1237 // FIXME: Try to repaint less. 1238 update(); 1239 } 1240} 1241 1242void TextEditor::context_menu_event(ContextMenuEvent& event) 1243{ 1244 if (!m_context_menu) { 1245 m_context_menu = Menu::construct(); 1246 m_context_menu->add_action(undo_action()); 1247 m_context_menu->add_action(redo_action()); 1248 m_context_menu->add_separator(); 1249 m_context_menu->add_action(cut_action()); 1250 m_context_menu->add_action(copy_action()); 1251 m_context_menu->add_action(paste_action()); 1252 m_context_menu->add_action(delete_action()); 1253 if (is_multi_line()) { 1254 m_context_menu->add_separator(); 1255 m_context_menu->add_action(go_to_line_action()); 1256 } 1257 if (!m_custom_context_menu_actions.is_empty()) { 1258 m_context_menu->add_separator(); 1259 for (auto& action : m_custom_context_menu_actions) { 1260 m_context_menu->add_action(action); 1261 } 1262 } 1263 } 1264 m_context_menu->popup(event.screen_position()); 1265} 1266 1267void TextEditor::set_text_alignment(Gfx::TextAlignment alignment) 1268{ 1269 if (m_text_alignment == alignment) 1270 return; 1271 m_text_alignment = alignment; 1272 update(); 1273} 1274 1275void TextEditor::resize_event(ResizeEvent& event) 1276{ 1277 ScrollableWidget::resize_event(event); 1278 update_content_size(); 1279 recompute_all_visual_lines(); 1280} 1281 1282void TextEditor::theme_change_event(ThemeChangeEvent& event) 1283{ 1284 ScrollableWidget::theme_change_event(event); 1285 if (m_highlighter) 1286 m_highlighter->rehighlight(palette()); 1287} 1288 1289void TextEditor::set_selection(const TextRange& selection) 1290{ 1291 if (m_selection == selection) 1292 return; 1293 m_selection = selection; 1294 set_cursor(m_selection.end()); 1295 scroll_position_into_view(normalized_selection().start()); 1296 update(); 1297} 1298 1299void TextEditor::clear_selection() 1300{ 1301 if (!has_selection()) 1302 return; 1303 m_selection.clear(); 1304 update(); 1305} 1306 1307void TextEditor::recompute_all_visual_lines() 1308{ 1309 int y_offset = 0; 1310 for (size_t line_index = 0; line_index < line_count(); ++line_index) { 1311 recompute_visual_lines(line_index); 1312 m_line_visual_data[line_index].visual_rect.set_y(y_offset); 1313 y_offset += m_line_visual_data[line_index].visual_rect.height(); 1314 } 1315 1316 update_content_size(); 1317} 1318 1319void TextEditor::ensure_cursor_is_valid() 1320{ 1321 auto new_cursor = m_cursor; 1322 if (new_cursor.line() >= line_count()) 1323 new_cursor.set_line(line_count() - 1); 1324 if (new_cursor.column() > line(new_cursor.line()).length()) 1325 new_cursor.set_column(line(new_cursor.line()).length()); 1326 if (m_cursor != new_cursor) 1327 set_cursor(new_cursor); 1328} 1329 1330size_t TextEditor::visual_line_containing(size_t line_index, size_t column) const 1331{ 1332 size_t visual_line_index = 0; 1333 for_each_visual_line(line_index, [&](const Gfx::Rect&, const StringView& view, size_t start_of_visual_line) { 1334 if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length())) 1335 return IterationDecision::Break; 1336 ++visual_line_index; 1337 return IterationDecision::Continue; 1338 }); 1339 return visual_line_index; 1340} 1341 1342void TextEditor::recompute_visual_lines(size_t line_index) 1343{ 1344 auto& line = document().line(line_index); 1345 auto& visual_data = m_line_visual_data[line_index]; 1346 1347 visual_data.visual_line_breaks.clear_with_capacity(); 1348 1349 int available_width = visible_text_rect_in_inner_coordinates().width(); 1350 1351 if (is_line_wrapping_enabled()) { 1352 int line_width_so_far = 0; 1353 1354 for (size_t i = 0; i < line.length(); ++i) { 1355 auto ch = line.characters()[i]; 1356 auto glyph_width = font().glyph_width(ch); 1357 if ((line_width_so_far + glyph_width) > available_width) { 1358 visual_data.visual_line_breaks.append(i); 1359 line_width_so_far = glyph_width; 1360 continue; 1361 } 1362 line_width_so_far += glyph_width; 1363 } 1364 } 1365 1366 visual_data.visual_line_breaks.append(line.length()); 1367 1368 if (is_line_wrapping_enabled()) 1369 visual_data.visual_rect = { m_horizontal_content_padding, 0, available_width, static_cast<int>(visual_data.visual_line_breaks.size()) * line_height() }; 1370 else 1371 visual_data.visual_rect = { m_horizontal_content_padding, 0, font().width(line.view()), line_height() }; 1372} 1373 1374template<typename Callback> 1375void TextEditor::for_each_visual_line(size_t line_index, Callback callback) const 1376{ 1377 auto editor_visible_text_rect = visible_text_rect_in_inner_coordinates(); 1378 size_t start_of_line = 0; 1379 size_t visual_line_index = 0; 1380 1381 auto& line = document().line(line_index); 1382 auto& visual_data = m_line_visual_data[line_index]; 1383 1384 for (auto visual_line_break : visual_data.visual_line_breaks) { 1385 auto visual_line_view = StringView(line.characters() + start_of_line, visual_line_break - start_of_line); 1386 Gfx::Rect visual_line_rect { 1387 visual_data.visual_rect.x(), 1388 visual_data.visual_rect.y() + ((int)visual_line_index * line_height()), 1389 font().width(visual_line_view), 1390 line_height() 1391 }; 1392 if (is_right_text_alignment(text_alignment())) 1393 visual_line_rect.set_right_without_resize(editor_visible_text_rect.right()); 1394 if (!is_multi_line()) 1395 visual_line_rect.center_vertically_within(editor_visible_text_rect); 1396 if (callback(visual_line_rect, visual_line_view, start_of_line) == IterationDecision::Break) 1397 break; 1398 start_of_line = visual_line_break; 1399 ++visual_line_index; 1400 } 1401} 1402 1403void TextEditor::set_line_wrapping_enabled(bool enabled) 1404{ 1405 if (m_line_wrapping_enabled == enabled) 1406 return; 1407 1408 m_line_wrapping_enabled = enabled; 1409 horizontal_scrollbar().set_visible(!m_line_wrapping_enabled); 1410 update_content_size(); 1411 recompute_all_visual_lines(); 1412 update(); 1413} 1414 1415void TextEditor::add_custom_context_menu_action(Action& action) 1416{ 1417 m_custom_context_menu_actions.append(action); 1418} 1419 1420void TextEditor::did_change_font() 1421{ 1422 vertical_scrollbar().set_step(line_height()); 1423 recompute_all_visual_lines(); 1424 update(); 1425 Widget::did_change_font(); 1426} 1427 1428void TextEditor::document_did_append_line() 1429{ 1430 m_line_visual_data.append(make<LineVisualData>()); 1431 recompute_all_visual_lines(); 1432 update(); 1433} 1434 1435void TextEditor::document_did_remove_line(size_t line_index) 1436{ 1437 m_line_visual_data.remove(line_index); 1438 recompute_all_visual_lines(); 1439 update(); 1440} 1441 1442void TextEditor::document_did_remove_all_lines() 1443{ 1444 m_line_visual_data.clear(); 1445 recompute_all_visual_lines(); 1446 update(); 1447} 1448 1449void TextEditor::document_did_insert_line(size_t line_index) 1450{ 1451 m_line_visual_data.insert(line_index, make<LineVisualData>()); 1452 recompute_all_visual_lines(); 1453 update(); 1454} 1455 1456void TextEditor::document_did_change() 1457{ 1458 did_change(); 1459 update(); 1460} 1461 1462void TextEditor::document_did_set_text() 1463{ 1464 m_line_visual_data.clear(); 1465 for (size_t i = 0; i < m_document->line_count(); ++i) 1466 m_line_visual_data.append(make<LineVisualData>()); 1467 document_did_change(); 1468} 1469 1470void TextEditor::document_did_set_cursor(const TextPosition& position) 1471{ 1472 set_cursor(position); 1473} 1474 1475void TextEditor::set_document(TextDocument& document) 1476{ 1477 if (m_document.ptr() == &document) 1478 return; 1479 if (m_document) 1480 m_document->unregister_client(*this); 1481 m_document = document; 1482 m_line_visual_data.clear(); 1483 for (size_t i = 0; i < m_document->line_count(); ++i) { 1484 m_line_visual_data.append(make<LineVisualData>()); 1485 } 1486 m_cursor = { 0, 0 }; 1487 if (has_selection()) 1488 m_selection.clear(); 1489 recompute_all_visual_lines(); 1490 update(); 1491 m_document->register_client(*this); 1492} 1493 1494void TextEditor::flush_pending_change_notification_if_needed() 1495{ 1496 if (!m_has_pending_change_notification) 1497 return; 1498 if (on_change) 1499 on_change(); 1500 if (m_highlighter) 1501 m_highlighter->rehighlight(palette()); 1502 m_has_pending_change_notification = false; 1503} 1504 1505const SyntaxHighlighter* TextEditor::syntax_highlighter() const 1506{ 1507 return m_highlighter.ptr(); 1508} 1509 1510void TextEditor::set_syntax_highlighter(OwnPtr<SyntaxHighlighter> highlighter) 1511{ 1512 if (m_highlighter) 1513 m_highlighter->detach(); 1514 m_highlighter = move(highlighter); 1515 if (m_highlighter) { 1516 m_highlighter->attach(*this); 1517 m_highlighter->rehighlight(palette()); 1518 } else 1519 document().set_spans({}); 1520} 1521 1522int TextEditor::line_height() const 1523{ 1524 return font().glyph_height() + m_line_spacing; 1525} 1526 1527int TextEditor::glyph_width() const 1528{ 1529 return font().glyph_width('x'); 1530} 1531 1532}