GTextEditor.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. #include <LibGUI/GTextEditor.h>
  2. #include <LibGUI/GScrollBar.h>
  3. #include <LibGUI/GFontDatabase.h>
  4. #include <SharedGraphics/Painter.h>
  5. #include <Kernel/KeyCode.h>
  6. #include <unistd.h>
  7. #include <fcntl.h>
  8. #include <stdio.h>
  9. GTextEditor::GTextEditor(GWidget* parent)
  10. : GWidget(parent)
  11. {
  12. set_font(GFontDatabase::the().get_by_name("Csilla Thin"));
  13. set_fill_with_background_color(false);
  14. m_vertical_scrollbar = new GScrollBar(Orientation::Vertical, this);
  15. m_vertical_scrollbar->set_step(4);
  16. m_vertical_scrollbar->on_change = [this] (int) {
  17. update();
  18. };
  19. m_horizontal_scrollbar = new GScrollBar(Orientation::Horizontal, this);
  20. m_horizontal_scrollbar->set_step(4);
  21. m_horizontal_scrollbar->set_big_step(30);
  22. m_horizontal_scrollbar->on_change = [this] (int) {
  23. update();
  24. };
  25. m_lines.append(make<Line>());
  26. }
  27. GTextEditor::~GTextEditor()
  28. {
  29. }
  30. void GTextEditor::set_text(const String& text)
  31. {
  32. m_lines.clear();
  33. int start_of_current_line = 0;
  34. auto add_line = [&] (int current_position) {
  35. int line_length = current_position - start_of_current_line;
  36. auto line = make<Line>();
  37. if (line_length)
  38. line->set_text(text.substring(start_of_current_line, current_position - start_of_current_line));
  39. m_lines.append(move(line));
  40. start_of_current_line = current_position + 1;
  41. };
  42. int i = 0;
  43. for (i = 0; i < text.length(); ++i) {
  44. if (text[i] == '\n')
  45. add_line(i);
  46. }
  47. add_line(i);
  48. set_cursor(0, 0);
  49. update();
  50. }
  51. void GTextEditor::resize_event(GResizeEvent& event)
  52. {
  53. update_scrollbar_ranges();
  54. m_vertical_scrollbar->set_relative_rect(event.size().width() - m_vertical_scrollbar->preferred_size().width(), 0, m_vertical_scrollbar->preferred_size().width(), event.size().height() - m_horizontal_scrollbar->preferred_size().height());
  55. m_horizontal_scrollbar->set_relative_rect(0, event.size().height() - m_horizontal_scrollbar->preferred_size().height(), event.size().width() - m_vertical_scrollbar->preferred_size().width(), m_horizontal_scrollbar->preferred_size().height());
  56. }
  57. void GTextEditor::update_scrollbar_ranges()
  58. {
  59. int available_height = height() - m_horizontal_scrollbar->height();
  60. int excess_height = max(0, (content_height() + padding() * 2) - available_height);
  61. m_vertical_scrollbar->set_range(0, excess_height);
  62. int available_width = width() - m_vertical_scrollbar->width() - ruler_width();
  63. int excess_width = max(0, (content_width() + padding() * 2) - available_width);
  64. m_horizontal_scrollbar->set_range(0, excess_width);
  65. m_vertical_scrollbar->set_big_step(visible_content_rect().height());
  66. }
  67. int GTextEditor::content_height() const
  68. {
  69. return line_count() * line_height();
  70. }
  71. int GTextEditor::content_width() const
  72. {
  73. // FIXME: Cache this somewhere.
  74. int max_width = 0;
  75. for (auto& line : m_lines)
  76. max_width = max(line->width(font()), max_width);
  77. return max_width;
  78. }
  79. GTextPosition GTextEditor::text_position_at(const Point& a_position) const
  80. {
  81. auto position = a_position;
  82. position.move_by(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value());
  83. position.move_by(-(padding() + ruler_width()), -padding());
  84. int line_index = position.y() / line_height();
  85. int column_index = position.x() / glyph_width();
  86. line_index = min(line_index, line_count() - 1);
  87. column_index = min(column_index, m_lines[line_index]->length());
  88. return { line_index, column_index };
  89. }
  90. void GTextEditor::mousedown_event(GMouseEvent& event)
  91. {
  92. set_cursor(text_position_at(event.position()));
  93. // FIXME: Allow mouse selection!
  94. if (m_selection_start.is_valid()) {
  95. m_selection_start = { };
  96. update();
  97. }
  98. }
  99. int GTextEditor::ruler_width() const
  100. {
  101. // FIXME: Resize based on needed space.
  102. return 5 * font().glyph_width('x') + 4;
  103. }
  104. Rect GTextEditor::ruler_content_rect(int line_index) const
  105. {
  106. return {
  107. 0 - ruler_width() - padding() + m_horizontal_scrollbar->value(),
  108. line_index * line_height(),
  109. ruler_width(),
  110. line_height()
  111. };
  112. }
  113. void GTextEditor::paint_event(GPaintEvent& event)
  114. {
  115. Painter painter(*this);
  116. painter.set_clip_rect(event.rect());
  117. painter.fill_rect(event.rect(), Color::White);
  118. Rect ruler_rect { 0, 0, ruler_width(), height() - m_horizontal_scrollbar->height()};
  119. painter.fill_rect(ruler_rect, Color::LightGray);
  120. painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), Color::DarkGray);
  121. painter.translate(-m_horizontal_scrollbar->value(), -m_vertical_scrollbar->value());
  122. painter.translate(padding() + ruler_width(), padding());
  123. int exposed_width = max(content_width(), width());
  124. int first_visible_line = text_position_at(event.rect().top_left()).line();
  125. int last_visible_line = text_position_at(event.rect().bottom_right()).line();
  126. auto normalized_selection_start = m_selection_start;
  127. auto normalized_selection_end = m_cursor;
  128. if (m_cursor < m_selection_start)
  129. swap(normalized_selection_start, normalized_selection_end);
  130. bool has_selection = m_selection_start.is_valid();
  131. painter.set_font(Font::default_font());
  132. for (int i = first_visible_line; i <= last_visible_line; ++i) {
  133. bool is_current_line = i == m_cursor.line();
  134. auto ruler_line_rect = ruler_content_rect(i);
  135. Color text_color = Color::MidGray;
  136. if (is_current_line) {
  137. painter.set_font(Font::default_bold_font());
  138. text_color = Color::DarkGray;
  139. }
  140. painter.draw_text(ruler_line_rect.shrunken(2, 0), String::format("%u", i), TextAlignment::CenterRight, text_color);
  141. if (is_current_line)
  142. painter.set_font(Font::default_font());
  143. }
  144. painter.set_font(font());
  145. painter.set_clip_rect({ ruler_rect.right() + 1, 0, width() - m_vertical_scrollbar->width() - ruler_width(), height() - m_horizontal_scrollbar->height() });
  146. for (int i = first_visible_line; i <= last_visible_line; ++i) {
  147. auto& line = *m_lines[i];
  148. auto line_rect = line_content_rect(i);
  149. line_rect.set_width(exposed_width);
  150. if (i == m_cursor.line())
  151. painter.fill_rect(line_rect, Color(230, 230, 230));
  152. painter.draw_text(line_rect, line.characters(), line.length(), TextAlignment::CenterLeft, Color::Black);
  153. bool line_has_selection = has_selection && i >= normalized_selection_start.line() && i <= normalized_selection_end.line();
  154. if (line_has_selection) {
  155. int selection_start_column_on_line = normalized_selection_start.line() == i ? normalized_selection_start.column() : 0;
  156. int selection_end_column_on_line = normalized_selection_end.line() == i ? normalized_selection_end.column() : line.length();
  157. int selection_left = selection_start_column_on_line * font().glyph_width('x');
  158. int selection_right = line_rect.left() + selection_end_column_on_line * font().glyph_width('x');
  159. Rect selection_rect { selection_left, line_rect.y(), selection_right - selection_left, line_rect.height() };
  160. painter.fill_rect(selection_rect, Color::from_rgb(0x955233));
  161. painter.draw_text(selection_rect, line.characters() + selection_start_column_on_line, line.length() - selection_start_column_on_line - (line.length() - selection_end_column_on_line), TextAlignment::CenterLeft, Color::White);
  162. }
  163. }
  164. if (is_focused() && m_cursor_state)
  165. painter.fill_rect(cursor_content_rect(), Color::Red);
  166. painter.clear_clip_rect();
  167. painter.set_clip_rect(event.rect());
  168. painter.translate(0 - padding() - ruler_width(), -padding());
  169. painter.translate(m_horizontal_scrollbar->value(), m_vertical_scrollbar->value());
  170. painter.fill_rect({ m_horizontal_scrollbar->relative_rect().top_right().translated(1, 0), { m_vertical_scrollbar->preferred_size().width(), m_horizontal_scrollbar->preferred_size().height() } }, Color::LightGray);
  171. if (is_focused()) {
  172. Rect item_area_rect { 0, 0, width() - m_vertical_scrollbar->width(), height() - m_horizontal_scrollbar->height() };
  173. painter.draw_rect(item_area_rect, Color::from_rgb(0x84351a));
  174. };
  175. }
  176. void GTextEditor::toggle_selection_if_needed_for_event(const GKeyEvent& event)
  177. {
  178. if (event.shift() && !m_selection_start.is_valid()) {
  179. m_selection_start = m_cursor;
  180. update();
  181. return;
  182. }
  183. if (!event.shift() && m_selection_start.is_valid()) {
  184. m_selection_start = { };
  185. update();
  186. return;
  187. }
  188. }
  189. void GTextEditor::keydown_event(GKeyEvent& event)
  190. {
  191. if (event.key() == KeyCode::Key_Up) {
  192. if (m_cursor.line() > 0) {
  193. int new_line = m_cursor.line() - 1;
  194. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  195. toggle_selection_if_needed_for_event(event);
  196. set_cursor(new_line, new_column);
  197. }
  198. return;
  199. }
  200. if (event.key() == KeyCode::Key_Down) {
  201. if (m_cursor.line() < (m_lines.size() - 1)) {
  202. int new_line = m_cursor.line() + 1;
  203. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  204. toggle_selection_if_needed_for_event(event);
  205. set_cursor(new_line, new_column);
  206. }
  207. return;
  208. }
  209. if (event.key() == KeyCode::Key_PageUp) {
  210. if (m_cursor.line() > 0) {
  211. int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height());
  212. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  213. toggle_selection_if_needed_for_event(event);
  214. set_cursor(new_line, new_column);
  215. }
  216. return;
  217. }
  218. if (event.key() == KeyCode::Key_PageDown) {
  219. if (m_cursor.line() < (m_lines.size() - 1)) {
  220. int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
  221. int new_column = min(m_cursor.column(), m_lines[new_line]->length());
  222. toggle_selection_if_needed_for_event(event);
  223. set_cursor(new_line, new_column);
  224. }
  225. return;
  226. }
  227. if (event.key() == KeyCode::Key_Left) {
  228. if (m_cursor.column() > 0) {
  229. int new_column = m_cursor.column() - 1;
  230. toggle_selection_if_needed_for_event(event);
  231. set_cursor(m_cursor.line(), new_column);
  232. }
  233. return;
  234. }
  235. if (event.key() == KeyCode::Key_Right) {
  236. if (m_cursor.column() < current_line().length()) {
  237. int new_column = m_cursor.column() + 1;
  238. toggle_selection_if_needed_for_event(event);
  239. set_cursor(m_cursor.line(), new_column);
  240. }
  241. return;
  242. }
  243. if (event.key() == KeyCode::Key_Home) {
  244. toggle_selection_if_needed_for_event(event);
  245. set_cursor(m_cursor.line(), 0);
  246. return;
  247. }
  248. if (event.key() == KeyCode::Key_End) {
  249. toggle_selection_if_needed_for_event(event);
  250. set_cursor(m_cursor.line(), current_line().length());
  251. return;
  252. }
  253. if (event.key() == KeyCode::Key_Home) {
  254. toggle_selection_if_needed_for_event(event);
  255. set_cursor(0, 0);
  256. return;
  257. }
  258. if (event.key() == KeyCode::Key_End) {
  259. toggle_selection_if_needed_for_event(event);
  260. set_cursor(line_count() - 1, m_lines[line_count() - 1]->length());
  261. return;
  262. }
  263. if (!event.modifiers() && event.key() == KeyCode::Key_Backspace) {
  264. if (m_cursor.column() > 0) {
  265. // Backspace within line
  266. current_line().remove(m_cursor.column() - 1);
  267. update_scrollbar_ranges();
  268. set_cursor(m_cursor.line(), m_cursor.column() - 1);
  269. return;
  270. }
  271. if (m_cursor.column() == 0 && m_cursor.line() != 0) {
  272. // Backspace at column 0; merge with previous line
  273. auto& previous_line = *m_lines[m_cursor.line() - 1];
  274. int previous_length = previous_line.length();
  275. previous_line.append(current_line().characters(), current_line().length());
  276. m_lines.remove(m_cursor.line());
  277. update_scrollbar_ranges();
  278. update();
  279. set_cursor(m_cursor.line() - 1, previous_length);
  280. return;
  281. }
  282. return;
  283. }
  284. if (!event.modifiers() && event.key() == KeyCode::Key_Delete) {
  285. if (m_cursor.column() < current_line().length()) {
  286. // Delete within line
  287. current_line().remove(m_cursor.column());
  288. update_scrollbar_ranges();
  289. update_cursor();
  290. return;
  291. }
  292. if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) {
  293. // Delete at end of line; merge with next line
  294. auto& next_line = *m_lines[m_cursor.line() + 1];
  295. int previous_length = current_line().length();
  296. current_line().append(next_line.characters(), next_line.length());
  297. m_lines.remove(m_cursor.line() + 1);
  298. update_scrollbar_ranges();
  299. update();
  300. set_cursor(m_cursor.line(), previous_length);
  301. return;
  302. }
  303. return;
  304. }
  305. if (!event.text().is_empty())
  306. insert_at_cursor(event.text()[0]);
  307. return GWidget::keydown_event(event);
  308. }
  309. void GTextEditor::insert_at_cursor(char ch)
  310. {
  311. bool at_head = m_cursor.column() == 0;
  312. bool at_tail = m_cursor.column() == current_line().length();
  313. if (ch == '\n') {
  314. if (at_tail || at_head) {
  315. m_lines.insert(m_cursor.line() + (at_tail ? 1 : 0), make<Line>());
  316. update_scrollbar_ranges();
  317. update();
  318. set_cursor(m_cursor.line() + 1, 0);
  319. return;
  320. }
  321. auto new_line = make<Line>();
  322. new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
  323. current_line().truncate(m_cursor.column());
  324. m_lines.insert(m_cursor.line() + 1, move(new_line));
  325. update_scrollbar_ranges();
  326. update();
  327. set_cursor(m_cursor.line() + 1, 0);
  328. return;
  329. }
  330. current_line().insert(m_cursor.column(), ch);
  331. update_scrollbar_ranges();
  332. set_cursor(m_cursor.line(), m_cursor.column() + 1);
  333. update_cursor();
  334. }
  335. Rect GTextEditor::visible_content_rect() const
  336. {
  337. return {
  338. m_horizontal_scrollbar->value(),
  339. m_vertical_scrollbar->value(),
  340. width() - m_vertical_scrollbar->width() - padding() * 2 - ruler_width(),
  341. height() - m_horizontal_scrollbar->height() - padding() * 2
  342. };
  343. }
  344. Rect GTextEditor::cursor_content_rect() const
  345. {
  346. if (!m_cursor.is_valid())
  347. return { };
  348. ASSERT(!m_lines.is_empty());
  349. ASSERT(m_cursor.column() <= (current_line().length() + 1));
  350. return { m_cursor.column() * glyph_width(), m_cursor.line() * line_height(), 1, line_height() };
  351. }
  352. Rect GTextEditor::line_widget_rect(int line_index) const
  353. {
  354. ASSERT(m_horizontal_scrollbar);
  355. ASSERT(m_vertical_scrollbar);
  356. auto rect = line_content_rect(line_index);
  357. rect.move_by(-(m_horizontal_scrollbar->value() - padding()), -(m_vertical_scrollbar->value() - padding()));
  358. rect.set_width(rect.width() + 1); // Add 1 pixel for when the cursor is on the end.
  359. rect.intersect(this->rect());
  360. // This feels rather hackish, but extend the rect to the edge of the content view:
  361. rect.set_right(m_vertical_scrollbar->relative_rect().left() - 1);
  362. return rect;
  363. }
  364. void GTextEditor::scroll_cursor_into_view()
  365. {
  366. auto visible_content_rect = this->visible_content_rect();
  367. auto rect = cursor_content_rect();
  368. if (visible_content_rect.is_empty())
  369. return;
  370. if (visible_content_rect.contains(rect))
  371. return;
  372. if (rect.top() < visible_content_rect.top())
  373. m_vertical_scrollbar->set_value(rect.top());
  374. else if (rect.bottom() > visible_content_rect.bottom())
  375. m_vertical_scrollbar->set_value(rect.bottom() - visible_content_rect.height());
  376. if (rect.left() < visible_content_rect.left())
  377. m_horizontal_scrollbar->set_value(rect.left());
  378. else if (rect.right() > visible_content_rect.right())
  379. m_horizontal_scrollbar->set_value(rect.right() - visible_content_rect.width());
  380. update();
  381. }
  382. Rect GTextEditor::line_content_rect(int line_index) const
  383. {
  384. return {
  385. 0,
  386. line_index * line_height(),
  387. content_width(),
  388. line_height()
  389. };
  390. }
  391. void GTextEditor::update_cursor()
  392. {
  393. update(line_widget_rect(m_cursor.line()));
  394. }
  395. void GTextEditor::set_cursor(int line, int column)
  396. {
  397. set_cursor({ line, column });
  398. }
  399. void GTextEditor::set_cursor(const GTextPosition& position)
  400. {
  401. if (m_cursor == position)
  402. return;
  403. auto old_cursor_line_rect = line_widget_rect(m_cursor.line());
  404. m_cursor = position;
  405. m_cursor_state = true;
  406. scroll_cursor_into_view();
  407. update(old_cursor_line_rect);
  408. update_cursor();
  409. if (on_cursor_change)
  410. on_cursor_change(*this);
  411. }
  412. void GTextEditor::focusin_event(GEvent&)
  413. {
  414. update_cursor();
  415. start_timer(500);
  416. }
  417. void GTextEditor::focusout_event(GEvent&)
  418. {
  419. stop_timer();
  420. }
  421. void GTextEditor::timer_event(GTimerEvent&)
  422. {
  423. m_cursor_state = !m_cursor_state;
  424. if (is_focused())
  425. update_cursor();
  426. }
  427. GTextEditor::Line::Line()
  428. {
  429. m_text.append(0);
  430. }
  431. void GTextEditor::Line::set_text(const String& text)
  432. {
  433. if (text.length() == length() && !memcmp(text.characters(), characters(), length()))
  434. return;
  435. m_text.resize(text.length() + 1);
  436. memcpy(m_text.data(), text.characters(), text.length() + 1);
  437. }
  438. int GTextEditor::Line::width(const Font& font) const
  439. {
  440. return font.glyph_width('x') * length();
  441. }
  442. void GTextEditor::Line::append(const char* characters, int length)
  443. {
  444. int old_length = m_text.size() - 1;
  445. m_text.resize(m_text.size() + length);
  446. memcpy(m_text.data() + old_length, characters, length);
  447. m_text.last() = 0;
  448. }
  449. void GTextEditor::Line::append(char ch)
  450. {
  451. insert(length(), ch);
  452. }
  453. void GTextEditor::Line::prepend(char ch)
  454. {
  455. insert(0, ch);
  456. }
  457. void GTextEditor::Line::insert(int index, char ch)
  458. {
  459. if (index == length()) {
  460. m_text.last() = ch;
  461. m_text.append(0);
  462. } else {
  463. m_text.insert(index, move(ch));
  464. }
  465. }
  466. void GTextEditor::Line::remove(int index)
  467. {
  468. if (index == length()) {
  469. m_text.take_last();
  470. m_text.last() = 0;
  471. } else {
  472. m_text.remove(index);
  473. }
  474. }
  475. void GTextEditor::Line::truncate(int length)
  476. {
  477. m_text.resize(length + 1);
  478. m_text.last() = 0;
  479. }
  480. bool GTextEditor::write_to_file(const String& path)
  481. {
  482. int fd = open(path.characters(), O_WRONLY | O_CREAT, 0666);
  483. if (fd < 0) {
  484. perror("open");
  485. return false;
  486. }
  487. for (int i = 0; i < m_lines.size(); ++i) {
  488. auto& line = *m_lines[i];
  489. if (line.length()) {
  490. ssize_t nwritten = write(fd, line.characters(), line.length());
  491. if (nwritten < 0) {
  492. perror("write");
  493. close(fd);
  494. return false;
  495. }
  496. }
  497. if (i != m_lines.size() - 1) {
  498. char ch = '\n';
  499. ssize_t nwritten = write(fd, &ch, 1);
  500. if (nwritten != 1) {
  501. perror("write");
  502. close(fd);
  503. return false;
  504. }
  505. }
  506. }
  507. close(fd);
  508. return true;
  509. }